From 49dc080c65918f1729223e17900936bd7e3b794b Mon Sep 17 00:00:00 2001 From: Leo Galambos Date: Tue, 28 Jul 2026 19:20:30 +0200 Subject: [PATCH] refactor!: consolidate crypto architecture and security model * make ZeroEchoSession the sole policy, audit, and runtime boundary * replace combined key builders with operation-specific SPI and typed metadata * remove obsolete pre-release compatibility APIs and global crypto operations * finalize JCA agreement contexts and replace inheritance with composition * harden secret lifecycle, key destruction, hybrid KEX, PBKDF2, and audit handling * standardize PairSeq I/O and introduce immutable validated value types * migrate app, ext, samples, and required pki integration points * expand correctness, security, concurrency, and malformed-input coverage BREAKING CHANGE: removes deprecated pre-release global configuration, legacy context factories, combined key-builder contracts, String-based password APIs, unchecked PairSeq writing, BlockGeometry public fields, and other compatibility facades. --- app/src/main/java/zeroecho/Guard.java | 282 ++-- app/src/main/java/zeroecho/Kem.java | 10 +- .../java/zeroecho/KeyStoreManagement.java | 277 ++-- app/src/main/java/zeroecho/Tag.java | 12 +- app/src/test/java/zeroecho/GuardTest.java | 54 +- app/src/test/java/zeroecho/KemTest.java | 2 +- .../java/zeroecho/KeyStoreManagementTest.java | 50 +- app/src/test/java/zeroecho/TagTest.java | 2 +- .../covert/jpeg/JpegExifIntegrationTest.java | 20 +- .../main/java/zeroecho/core/Capability.java | 110 +- .../java/zeroecho/core/CryptoAlgorithm.java | 646 +++----- .../java/zeroecho/core/CryptoAlgorithms.java | 603 +------- .../java/zeroecho/core/CryptoCatalog.java | 102 +- .../main/java/zeroecho/core/KeyOperation.java | 26 + .../java/zeroecho/core/KeyOperationInfo.java | 47 + .../core/alg/AbstractCryptoAlgorithm.java | 29 +- .../zeroecho/core/alg/aes/AesAlgorithm.java | 38 +- .../core/alg/aes/AesCipherContext.java | 6 +- .../core/alg/aes/AesKeyImportSpec.java | 123 +- .../zeroecho/core/alg/bike/BikeAlgorithm.java | 56 +- .../core/alg/bike/BikeKeyGenSpec.java | 2 +- .../core/alg/bike/BikePrivateKeySpec.java | 66 +- .../core/alg/bike/BikePublicKeySpec.java | 2 +- .../alg/chacha/AbstractChaChaAlgorithm.java | 39 +- .../alg/chacha/ChaCha20Poly1305Algorithm.java | 18 +- .../core/alg/chacha/ChaChaKeyImportSpec.java | 117 +- .../core/alg/chacha/package-info.java | 7 +- .../zeroecho/core/alg/cmce/CmceAlgorithm.java | 56 +- .../core/alg/cmce/CmceKeyGenSpec.java | 2 +- .../core/alg/cmce/CmcePrivateKeySpec.java | 68 +- .../zeroecho/core/alg/cmce/package-info.java | 4 +- .../agreement/GenericJcaAgreementContext.java | 39 +- .../GenericJcaMessageAgreementContext.java | 41 +- .../common/agreement/JcaAgreementEngine.java | 54 + .../eddsa/AbstractEdDSAKeyGenBuilder.java | 45 +- .../AbstractEncodedPrivateKeyBuilder.java | 40 +- .../AbstractEncodedPublicKeyBuilder.java | 32 +- .../sig/GenericJcaSignatureContext.java | 4 +- .../common/sig/SignatureInteropProfile.java | 2 +- .../sig/{Stream.java => SignatureStream.java} | 6 +- .../core/alg/common/sig/package-info.java | 2 +- .../zeroecho/core/alg/dh/DhAlgorithm.java | 46 +- .../zeroecho/core/alg/dh/DhKeyGenBuilder.java | 58 +- .../core/alg/dh/DhPrivateKeySpec.java | 79 +- .../zeroecho/core/alg/dh/DhPublicKeySpec.java | 2 +- .../java/zeroecho/core/alg/dh/DhSpec.java | 4 +- .../zeroecho/core/alg/dh/package-info.java | 11 +- .../zeroecho/core/alg/digest/DigestSpec.java | 2 +- .../core/alg/digest/Sha2Sha3Algorithm.java | 7 +- .../zeroecho/core/alg/ecdh/EcdhAlgorithm.java | 15 +- .../zeroecho/core/alg/ecdh/EcdhCurveSpec.java | 2 +- .../core/alg/ecdh/EcdhKeyGenBuilder.java | 44 +- .../core/alg/ecdsa/EcdsaAlgorithm.java | 18 +- .../core/alg/ecdsa/EcdsaKeyGenBuilder.java | 61 +- .../alg/ecdsa/EcdsaPrivateKeyBuilder.java | 66 +- .../core/alg/ecdsa/EcdsaPrivateKeySpec.java | 74 +- .../core/alg/ecdsa/EcdsaPublicKeyBuilder.java | 57 +- .../zeroecho/core/alg/ecdsa/package-info.java | 11 +- .../core/alg/ed25519/Ed25519Algorithm.java | 6 +- .../alg/ed25519/Ed25519KeyGenBuilder.java | 4 +- .../core/alg/ed25519/Ed25519KeyGenSpec.java | 3 +- .../alg/ed25519/Ed25519PrivateKeyBuilder.java | 4 +- .../alg/ed25519/Ed25519PrivateKeySpec.java | 79 +- .../alg/ed25519/Ed25519PublicKeyBuilder.java | 4 +- .../alg/ed25519/Ed25519PublicKeySpec.java | 2 +- .../core/alg/ed25519/package-info.java | 10 +- .../core/alg/ed448/Ed448Algorithm.java | 16 +- .../alg/ed448/Ed448PrivateKeyBuilder.java | 5 +- .../core/alg/ed448/Ed448PrivateKeySpec.java | 77 +- .../core/alg/ed448/Ed448PublicKeyBuilder.java | 5 +- .../zeroecho/core/alg/ed448/package-info.java | 13 +- .../core/alg/elgamal/ElgamalAlgorithm.java | 69 +- .../alg/elgamal/ElgamalCipherContext.java | 2 +- .../core/alg/elgamal/ElgamalEncSpec.java | 2 +- .../core/alg/elgamal/ElgamalKeyGenSpec.java | 2 +- .../core/alg/elgamal/ElgamalParamSpec.java | 2 +- .../alg/elgamal/ElgamalPrivateKeySpec.java | 77 +- .../alg/elgamal/ElgamalPublicKeySpec.java | 2 +- .../core/alg/elgamal/package-info.java | 6 +- .../core/alg/frodo/FrodoAlgorithm.java | 62 +- .../core/alg/frodo/FrodoPrivateKeySpec.java | 78 +- .../core/alg/frodo/FrodoPublicKeySpec.java | 8 +- .../zeroecho/core/alg/frodo/package-info.java | 5 +- .../zeroecho/core/alg/hmac/HmacAlgorithm.java | 34 +- .../core/alg/hmac/HmacKeyGenSpec.java | 4 +- .../core/alg/hmac/HmacKeyImportSpec.java | 119 +- .../core/alg/hmac/HmacMacContext.java | 6 +- .../java/zeroecho/core/alg/hmac/HmacSpec.java | 4 +- .../alg/hmac/{Stream.java => HmacStream.java} | 10 +- .../zeroecho/core/alg/hmac/package-info.java | 6 +- .../zeroecho/core/alg/hqc/HqcAlgorithm.java | 57 +- .../zeroecho/core/alg/hqc/HqcKeyGenSpec.java | 2 +- .../core/alg/hqc/HqcPrivateKeySpec.java | 70 +- .../core/alg/hqc/HqcPublicKeySpec.java | 2 +- .../zeroecho/core/alg/hqc/package-info.java | 5 +- .../core/alg/kyber/KyberAlgorithm.java | 119 +- .../core/alg/kyber/KyberKeyGenSpec.java | 4 +- .../core/alg/kyber/KyberPrivateKeySpec.java | 72 +- .../core/alg/kyber/KyberPublicKeySpec.java | 2 +- .../zeroecho/core/alg/kyber/package-info.java | 5 +- .../core/alg/mldsa/MldsaAlgorithm.java | 7 +- .../core/alg/mldsa/MldsaKeyGenBuilder.java | 28 +- .../alg/mldsa/MldsaPrivateKeyBuilder.java | 38 +- .../core/alg/mldsa/MldsaPrivateKeySpec.java | 96 +- .../core/alg/mldsa/MldsaPublicKeyBuilder.java | 30 +- .../zeroecho/core/alg/ntru/NtruAlgorithm.java | 104 +- .../core/alg/ntru/NtruKeyGenSpec.java | 2 +- .../core/alg/ntru/NtruPrivateKeySpec.java | 69 +- .../zeroecho/core/alg/ntru/package-info.java | 5 +- .../alg/ntruprime/NtrulPrimeAlgorithm.java | 63 +- .../alg/ntruprime/NtrulPrimeKeyGenSpec.java | 11 +- .../ntruprime/NtrulPrimePrivateKeySpec.java | 68 +- .../ntruprime/NtrulPrimePublicKeySpec.java | 2 +- .../alg/ntruprime/SntruPrimeAlgorithm.java | 52 +- .../alg/ntruprime/SntruPrimeKeyGenSpec.java | 11 +- .../ntruprime/SntruPrimePrivateKeySpec.java | 68 +- .../ntruprime/SntruPrimePublicKeySpec.java | 2 +- .../core/alg/ntruprime/package-info.java | 5 +- .../zeroecho/core/alg/rsa/BlockGeometry.java | 57 +- .../zeroecho/core/alg/rsa/RsaAlgorithm.java | 56 +- .../core/alg/rsa/RsaCipherContext.java | 6 +- .../zeroecho/core/alg/rsa/RsaKeyGenSpec.java | 2 +- .../core/alg/rsa/RsaPrivateKeySpec.java | 77 +- .../core/alg/saber/SaberAlgorithm.java | 59 +- .../core/alg/saber/SaberKeyGenSpec.java | 4 +- .../core/alg/saber/SaberPrivateKeySpec.java | 71 +- .../core/alg/saber/SaberPublicKeySpec.java | 2 +- .../zeroecho/core/alg/saber/package-info.java | 5 +- .../core/alg/slhdsa/SlhDsaAlgorithm.java | 7 +- .../core/alg/slhdsa/SlhDsaKeyGenBuilder.java | 14 +- .../alg/slhdsa/SlhDsaPrivateKeyBuilder.java | 24 +- .../core/alg/slhdsa/SlhDsaPrivateKeySpec.java | 98 +- .../alg/slhdsa/SlhDsaPublicKeyBuilder.java | 16 +- .../core/alg/slhdsa/package-info.java | 3 +- .../alg/sphincsplus/SphincsPlusAlgorithm.java | 12 +- .../sphincsplus/SphincsPlusKeyGenBuilder.java | 51 +- .../SphincsPlusPrivateKeyBuilder.java | 58 +- .../SphincsPlusPrivateKeySpec.java | 100 +- .../SphincsPlusPublicKeyBuilder.java | 51 +- .../core/alg/sphincsplus/package-info.java | 9 +- .../zeroecho/core/alg/xdh/XdhAlgorithm.java | 50 +- .../core/alg/xdh/XdhKeyGenBuilder.java | 47 +- .../core/alg/xdh/XdhPrivateKeySpec.java | 72 +- .../java/zeroecho/core/alg/xdh/XdhSpec.java | 4 +- .../zeroecho/core/alg/xdh/package-info.java | 4 +- .../zeroecho/core/audit/AuditListener.java | 63 - .../zeroecho/core/audit/AuditListeners.java | 51 + .../java/zeroecho/core/audit/AuditMode.java | 31 + .../zeroecho/core/audit/AuditedContexts.java | 343 +++-- .../core/audit/JulAuditListenerStd.java | 132 +- .../core/context/AgreementContext.java | 4 +- .../zeroecho/core/context/CryptoContext.java | 15 +- .../core/context/EncryptionContext.java | 2 +- .../zeroecho/core/context/KemContext.java | 2 +- .../core/context/MessageAgreementContext.java | 2 +- .../core/err/UnsupportedRoleException.java | 10 +- .../core/err/UnsupportedSpecException.java | 6 +- .../java/zeroecho/core/err/package-info.java | 2 +- .../io/AbstractChunkTransformInputStream.java | 93 +- .../io/CipherTransformInputStreamBuilder.java | 44 +- .../zeroecho/core/io/SmartBlockStream.java | 6 +- .../core/io/SmartContinuousBlockStream.java | 12 +- .../core/io/SmartPaddedBlockStream.java | 9 +- lib/src/main/java/zeroecho/core/io/Util.java | 30 +- .../java/zeroecho/core/io/package-info.java | 6 +- .../java/zeroecho/core/marshal/PairSeq.java | 36 +- .../zeroecho/core/marshal/PairSeqCodec.java | 230 ++- .../zeroecho/core/policy/CryptoPolicy.java | 7 +- .../zeroecho/core/policy/package-info.java | 9 +- .../zeroecho/core/spec/AlgorithmKeySpec.java | 3 +- .../java/zeroecho/core/spec/VoidSpec.java | 2 +- .../java/zeroecho/core/spec/package-info.java | 4 +- .../core/spi/AsymmetricKeyBuilder.java | 131 -- .../core/spi/AsymmetricKeyPairGenerator.java | 29 + .../core/spi/ContextConstructorKS.java | 96 -- .../zeroecho/core/spi/ContextFactoryKS.java | 39 + .../zeroecho/core/spi/PrivateKeyImporter.java | 29 + .../zeroecho/core/spi/PublicKeyImporter.java | 29 + .../core/spi/SymmetricKeyBuilder.java | 114 -- .../core/spi/SymmetricKeyGenerator.java | 30 + .../core/spi/SymmetricKeyImporter.java | 30 + .../java/zeroecho/core/spi/package-info.java | 156 +- .../zeroecho/core/storage/KeyringStore.java | 165 ++- .../zeroecho/core/storage/package-info.java | 11 +- .../zeroecho/core/tag/TagEngineBuilder.java | 136 +- .../core/util/GenerateCryptoCatalogTable.java | 39 +- .../main/java/zeroecho/sdk/KeyBuilders.java | 271 ++++ .../main/java/zeroecho/sdk/Pbkdf2Limits.java | 65 + .../java/zeroecho/sdk/ZeroEchoSession.java | 382 +++++ .../sdk/builders/HybridKexBuilder.java | 134 +- .../SignatureTrailerDataContentBuilder.java | 59 +- .../TagTrailerDataContentBuilder.java | 6 +- ...AbstractStreamingSignatureDataBuilder.java | 1302 ----------------- .../builders/alg/AesDataContentBuilder.java | 37 +- .../alg/ChaChaDataContentBuilder.java | 37 +- .../alg/DigestDataContentBuilder.java | 21 +- .../builders/alg/EcdsaDataContentBuilder.java | 315 ---- .../alg/Ed25519DataContentBuilder.java | 253 ---- .../builders/alg/Ed448DataContentBuilder.java | 274 ---- .../alg/ElgamalEncDataContentBuilder.java | 43 +- .../builders/alg/HmacDataContentBuilder.java | 41 +- .../builders/alg/KemDataContentBuilder.java | 168 ++- .../builders/alg/MldsaDataContentBuilder.java | 257 ---- .../alg/RsaEncDataContentBuilder.java | 39 +- .../alg/RsaSigDataContentBuilder.java | 42 +- .../alg/SlhDsaDataContentBuilder.java | 257 ---- .../alg/SphincsPlusDataContentBuilder.java | 300 ---- .../sdk/builders/alg/package-info.java | 11 +- .../zeroecho/sdk/builders/package-info.java | 10 +- .../sdk/content/builtin/SecretPassword.java | 214 ++- .../sdk/content/builtin/package-info.java | 15 +- .../java/zeroecho/sdk/guard/Decryptor.java | 268 +++- .../java/zeroecho/sdk/guard/EncCtxOpener.java | 54 +- .../zeroecho/sdk/guard/EncCtxRecipient.java | 23 +- .../java/zeroecho/sdk/guard/Encryptor.java | 198 ++- .../java/zeroecho/sdk/guard/KemCtxOpener.java | 81 +- .../zeroecho/sdk/guard/KemCtxRecipient.java | 75 +- .../zeroecho/sdk/guard/KemKeyDerivation.java | 38 + .../sdk/guard/MultiRecipientContent.java | 118 ++ .../MultiRecipientDataSourceBuilder.java | 235 ++- .../zeroecho/sdk/guard/PasswordOpener.java | 39 +- .../zeroecho/sdk/guard/PasswordRecipient.java | 111 +- .../zeroecho/sdk/guard/RecipientKekSizes.java | 39 + .../zeroecho/sdk/guard/RecipientOpener.java | 19 +- .../zeroecho/sdk/guard/UnlockMaterial.java | 178 +-- .../java/zeroecho/sdk/guard/package-info.java | 31 +- .../sdk/hybrid/derived/HybridDerived.java | 30 +- .../sdk/hybrid/kex/HybridKexContext.java | 118 +- .../sdk/hybrid/kex/HybridKexContexts.java | 130 +- .../sdk/hybrid/kex/HybridKexExporter.java | 116 +- .../sdk/hybrid/kex/HybridKexProfile.java | 22 +- .../signature/HybridSignatureContext.java | 31 +- .../signature/HybridSignatureContexts.java | 14 +- .../sdk/io/SignatureTrailerInputStream.java | 2 +- .../main/java/zeroecho/sdk/package-info.java | 7 +- lib/src/main/java/zeroecho/sdk/util/Kdf.java | 54 +- .../main/java/zeroecho/sdk/util/Password.java | 89 +- .../java/zeroecho/sdk/util/RandomSupport.java | 69 +- .../core/CapabilityValueSemanticsTest.java | 128 ++ .../zeroecho/core/CatalogContractTest.java | 81 +- .../core/CryptoAlgorithmsAuditWrapTest.java | 220 ++- .../zeroecho/core/CryptoArchitectureTest.java | 205 +++ .../core/SecretSpecLifecycleTest.java | 244 +++ .../zeroecho/core/TargetArchitectureTest.java | 154 ++ .../zeroecho/core/WrongKeySecurityTest.java | 55 + .../ZeroEchoSessionWrapIntegrationTest.java | 275 ++++ .../alg/aes/AesDecryptionSecurityTest.java | 233 +++ .../core/alg/aes/AesGcmCrossCheckTest.java | 6 +- .../core/alg/aes/AesLargeDataTest.java | 18 +- .../core/alg/aes/AesRandomSupportTest.java | 149 ++ .../core/alg/chacha/ChaChaLargeDataTest.java | 38 +- .../AgreementAlgorithmsRoundTripTest.java | 32 +- .../core/alg/ecdsa/EcdsaLargeDataTest.java | 8 +- .../alg/ed25519/Ed25519LargeDataTest.java | 6 +- .../core/alg/ed448/Ed448LargeDataTest.java | 6 +- .../alg/elgamal/ElgamalLargeDataTest.java | 16 +- .../core/alg/hmac/HmacLargeDataTest.java | 6 +- .../core/alg/mldsa/MldsaLargeDataTest.java | 8 +- .../core/alg/rsa/BlockGeometryTest.java | 79 + .../core/alg/rsa/RsaLargeDataTest.java | 12 +- .../core/alg/slhdsa/SlhDsaLargeDataTest.java | 8 +- .../audit/AuditedContextsAccessorTest.java | 272 ++++ .../audit/AuditedContextsRegressionTest.java | 261 ++++ .../JulAuditListenerStdSecurityTest.java | 172 +++ ...CipherTransformInputStreamBuilderTest.java | 107 ++ .../core/marshal/PairSeqCodecTest.java | 383 +++++ .../zeroecho/core/marshal/PairSeqTest.java | 138 ++ .../core/storage/KeyringStoreDynamicTest.java | 78 +- .../storage/KeyringStoreSecurityTest.java | 138 ++ .../sdk/ZeroEchoSessionDestroyKeyTest.java | 202 +++ .../sdk/builders/HybridKexBuilderTest.java | 190 ++- .../TagTrailerDataContentBuilderTest.java | 146 +- .../builders/alg/KemHybridRoundTripTest.java | 22 +- .../builders/alg/SessionBoundBuilderTest.java | 106 ++ .../content/builtin/SecretPasswordTest.java | 59 +- .../sdk/guard/DecryptorCekCleanupTest.java | 143 ++ .../sdk/guard/EncryptorCekAllocationTest.java | 218 +++ .../sdk/guard/KemRecipientLifecycleTest.java | 156 ++ .../sdk/guard/MultiRecipientEnvelopeTest.java | 346 ++++- .../sdk/guard/PasswordRecipientTest.java | 251 ++++ .../SessionRecipientOpenerContractTest.java | 116 ++ .../sdk/hybrid/derived/HybridDerivedTest.java | 22 +- .../kex/HybridKexExporterLifecycleTest.java | 84 ++ .../hybrid/kex/HybridKexFrameCodecTest.java | 196 +++ .../sdk/hybrid/kex/HybridKexTest.java | 20 +- .../hybrid/signature/HybridSignatureTest.java | 77 +- .../java/zeroecho/sdk/util/PasswordTest.java | 69 + .../ZeroEchoLibSignatureWorkflow.java | 29 +- ...gnatureWorkflowVerifyEncodedEcdsaTest.java | 2 +- ...LibSignatureWorkflowVerifyEncodedTest.java | 2 +- samples/src/test/java/demo/AesTest.java | 28 +- .../test/java/demo/AgreementVariantsTest.java | 27 +- .../test/java/demo/CombinedDeliveryTest.java | 47 +- .../java/demo/HybridDerivedAesDemoTest.java | 46 +- .../src/test/java/demo/HybridKexDemoTest.java | 46 +- .../test/java/demo/HybridSigningAesTest.java | 31 +- .../src/test/java/demo/PostQuantumTest.java | 14 +- .../src/test/java/demo/SigningAesTest.java | 22 +- 298 files changed, 12802 insertions(+), 8763 deletions(-) create mode 100644 lib/src/main/java/zeroecho/core/KeyOperation.java create mode 100644 lib/src/main/java/zeroecho/core/KeyOperationInfo.java create mode 100644 lib/src/main/java/zeroecho/core/alg/common/agreement/JcaAgreementEngine.java rename lib/src/main/java/zeroecho/core/alg/common/sig/{Stream.java => SignatureStream.java} (97%) rename lib/src/main/java/zeroecho/core/alg/hmac/{Stream.java => HmacStream.java} (95%) create mode 100644 lib/src/main/java/zeroecho/core/audit/AuditListeners.java create mode 100644 lib/src/main/java/zeroecho/core/audit/AuditMode.java delete mode 100644 lib/src/main/java/zeroecho/core/spi/AsymmetricKeyBuilder.java create mode 100644 lib/src/main/java/zeroecho/core/spi/AsymmetricKeyPairGenerator.java delete mode 100644 lib/src/main/java/zeroecho/core/spi/ContextConstructorKS.java create mode 100644 lib/src/main/java/zeroecho/core/spi/ContextFactoryKS.java create mode 100644 lib/src/main/java/zeroecho/core/spi/PrivateKeyImporter.java create mode 100644 lib/src/main/java/zeroecho/core/spi/PublicKeyImporter.java delete mode 100644 lib/src/main/java/zeroecho/core/spi/SymmetricKeyBuilder.java create mode 100644 lib/src/main/java/zeroecho/core/spi/SymmetricKeyGenerator.java create mode 100644 lib/src/main/java/zeroecho/core/spi/SymmetricKeyImporter.java create mode 100644 lib/src/main/java/zeroecho/sdk/KeyBuilders.java create mode 100644 lib/src/main/java/zeroecho/sdk/Pbkdf2Limits.java create mode 100644 lib/src/main/java/zeroecho/sdk/ZeroEchoSession.java delete mode 100644 lib/src/main/java/zeroecho/sdk/builders/alg/AbstractStreamingSignatureDataBuilder.java delete mode 100644 lib/src/main/java/zeroecho/sdk/builders/alg/EcdsaDataContentBuilder.java delete mode 100644 lib/src/main/java/zeroecho/sdk/builders/alg/Ed25519DataContentBuilder.java delete mode 100644 lib/src/main/java/zeroecho/sdk/builders/alg/Ed448DataContentBuilder.java delete mode 100644 lib/src/main/java/zeroecho/sdk/builders/alg/MldsaDataContentBuilder.java delete mode 100644 lib/src/main/java/zeroecho/sdk/builders/alg/SlhDsaDataContentBuilder.java delete mode 100644 lib/src/main/java/zeroecho/sdk/builders/alg/SphincsPlusDataContentBuilder.java create mode 100644 lib/src/main/java/zeroecho/sdk/guard/KemKeyDerivation.java create mode 100644 lib/src/main/java/zeroecho/sdk/guard/MultiRecipientContent.java create mode 100644 lib/src/main/java/zeroecho/sdk/guard/RecipientKekSizes.java create mode 100644 lib/src/test/java/zeroecho/core/CapabilityValueSemanticsTest.java create mode 100644 lib/src/test/java/zeroecho/core/CryptoArchitectureTest.java create mode 100644 lib/src/test/java/zeroecho/core/SecretSpecLifecycleTest.java create mode 100644 lib/src/test/java/zeroecho/core/TargetArchitectureTest.java create mode 100644 lib/src/test/java/zeroecho/core/WrongKeySecurityTest.java create mode 100644 lib/src/test/java/zeroecho/core/ZeroEchoSessionWrapIntegrationTest.java create mode 100644 lib/src/test/java/zeroecho/core/alg/aes/AesDecryptionSecurityTest.java create mode 100644 lib/src/test/java/zeroecho/core/alg/aes/AesRandomSupportTest.java create mode 100644 lib/src/test/java/zeroecho/core/alg/rsa/BlockGeometryTest.java create mode 100644 lib/src/test/java/zeroecho/core/audit/AuditedContextsAccessorTest.java create mode 100644 lib/src/test/java/zeroecho/core/audit/AuditedContextsRegressionTest.java create mode 100644 lib/src/test/java/zeroecho/core/audit/JulAuditListenerStdSecurityTest.java create mode 100644 lib/src/test/java/zeroecho/core/io/CipherTransformInputStreamBuilderTest.java create mode 100644 lib/src/test/java/zeroecho/core/marshal/PairSeqCodecTest.java create mode 100644 lib/src/test/java/zeroecho/core/marshal/PairSeqTest.java create mode 100644 lib/src/test/java/zeroecho/core/storage/KeyringStoreSecurityTest.java create mode 100644 lib/src/test/java/zeroecho/sdk/ZeroEchoSessionDestroyKeyTest.java create mode 100644 lib/src/test/java/zeroecho/sdk/builders/alg/SessionBoundBuilderTest.java create mode 100644 lib/src/test/java/zeroecho/sdk/guard/DecryptorCekCleanupTest.java create mode 100644 lib/src/test/java/zeroecho/sdk/guard/EncryptorCekAllocationTest.java create mode 100644 lib/src/test/java/zeroecho/sdk/guard/KemRecipientLifecycleTest.java create mode 100644 lib/src/test/java/zeroecho/sdk/guard/PasswordRecipientTest.java create mode 100644 lib/src/test/java/zeroecho/sdk/guard/SessionRecipientOpenerContractTest.java create mode 100644 lib/src/test/java/zeroecho/sdk/hybrid/kex/HybridKexExporterLifecycleTest.java create mode 100644 lib/src/test/java/zeroecho/sdk/hybrid/kex/HybridKexFrameCodecTest.java create mode 100644 lib/src/test/java/zeroecho/sdk/util/PasswordTest.java diff --git a/app/src/main/java/zeroecho/Guard.java b/app/src/main/java/zeroecho/Guard.java index c2d7c9d..35ec7e9 100644 --- a/app/src/main/java/zeroecho/Guard.java +++ b/app/src/main/java/zeroecho/Guard.java @@ -40,7 +40,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.GeneralSecurityException; -import java.security.SecureRandom; +import java.util.Arrays; import java.util.HexFormat; import java.util.Locale; @@ -52,17 +52,21 @@ import org.apache.commons.cli.OptionGroup; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.KeyUsage; import zeroecho.core.context.EncryptionContext; import zeroecho.core.context.KemContext; +import zeroecho.core.err.UnsupportedRoleException; import zeroecho.core.storage.KeyringStore; +import zeroecho.sdk.Pbkdf2Limits; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.sdk.builders.alg.AesDataContentBuilder; import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder; import zeroecho.sdk.builders.core.PlainFileBuilder; -import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.guard.MultiRecipientContent; import zeroecho.sdk.guard.MultiRecipientDataSourceBuilder; +import zeroecho.sdk.guard.RecipientKekSizes; import zeroecho.sdk.guard.UnlockMaterial; +import zeroecho.sdk.util.RandomSupport; /** * Guard is a unified subcommand that encrypts and decrypts using a @@ -97,6 +101,8 @@ import zeroecho.sdk.guard.UnlockMaterial; * ZeroEcho -G --encrypt in.bin --ks keys.txt \ * --to-alias alice --to-alias bob \ * --decoy-psw-rand 2 \ + * --pbkdf2-max "$PBKDF2_POLICY_MAX" \ + * --pbkdf2-hard-max "$PBKDF2_HARD_MAX" \ * --alg aes-gcm --tag-bits 128 --aad-hex 01ff * * # Decrypt with private key (payload is ChaCha20-Poly1305) @@ -124,7 +130,6 @@ public final class Guard { */ public static int main(final String[] args, final Options options) // NOPMD throws ParseException, IOException, GeneralSecurityException { - // ---- operation selection final Option OPT_ENCRYPT = Option.builder("e").longOpt("encrypt").hasArg().argName("in-file") .desc("Encrypt the given file").get(); @@ -177,7 +182,12 @@ public final class Guard { final Option OPT_PSW_SALT = Option.builder().longOpt("to-salt-len").hasArg().argName("bytes") .desc("PBKDF2 salt length for password recipients (default 16)").get(); final Option OPT_PSW_KEK = Option.builder().longOpt("to-kek-bytes").hasArg().argName("bytes") - .desc("Derived KEK length for password recipients (default 32)").get(); + .desc("Recipient KEK length: exactly 16 or 32 bytes (default 32)").get(); + final Option OPT_PBKDF2_MAX = Option.builder().longOpt("pbkdf2-max").hasArg().argName("iterations") + .desc("Operational PBKDF2 ceiling; required for password operations").get(); + final Option OPT_PBKDF2_HARD_MAX = Option.builder().longOpt("pbkdf2-hard-max").hasArg() + .argName("iterations") + .desc("Absolute decoded PBKDF2 safety ceiling; required for password operations").get(); // ---- decoys (all types) final Option OPT_DECOY_ALIAS = Option.builder().longOpt("decoy-alias").hasArg().argName("alias") @@ -215,6 +225,8 @@ public final class Guard { options.addOption(OPT_PSW_ITER); options.addOption(OPT_PSW_SALT); options.addOption(OPT_PSW_KEK); + options.addOption(OPT_PBKDF2_MAX); + options.addOption(OPT_PBKDF2_HARD_MAX); options.addOption(OPT_DECOY_ALIAS); options.addOption(OPT_DECOY_PSW); @@ -225,6 +237,10 @@ public final class Guard { final CommandLineParser parser = new DefaultParser(); final CommandLine cmd = parser.parse(options, args); + final boolean passwordOperation = cmd.hasOption(OPT_TO_PSW) || cmd.hasOption(OPT_DECOY_PSW) + || cmd.hasOption(OPT_DECOY_PSW_RAND) || cmd.hasOption(OPT_PASSWORD); + final ZeroEchoSession session = createSession(cmd, OPT_PBKDF2_MAX, OPT_PBKDF2_HARD_MAX, + passwordOperation); final boolean encrypt = cmd.hasOption(OPT_ENCRYPT); final Path inPath = Paths.get(cmd.getOptionValue(encrypt ? OPT_ENCRYPT : OPT_DECRYPT)); @@ -249,7 +265,7 @@ public final class Guard { final ChaChaDataContentBuilder chacha; switch (alg) { case "aes-gcm" -> { - aes = AesDataContentBuilder.builder().modeGcm(tagBits); + aes = AesDataContentBuilder.builder(session).modeGcm(tagBits); if (header) { aes.withHeader(); } @@ -259,28 +275,28 @@ public final class Guard { chacha = null; } case "aes-ctr" -> { - aes = AesDataContentBuilder.builder().modeCtr(); + aes = AesDataContentBuilder.builder(session).modeCtr(); if (header) { aes.withHeader(); } chacha = null; } case "aes-cbc-pkcs7" -> { - aes = AesDataContentBuilder.builder().modeCbcPkcs5(); + aes = AesDataContentBuilder.builder(session).modeCbcPkcs5(); if (header) { aes.withHeader(); } chacha = null; } case "aes-cbc-nopad" -> { - aes = AesDataContentBuilder.builder().modeCbcNoPadding(); + aes = AesDataContentBuilder.builder(session).modeCbcNoPadding(); if (header) { aes.withHeader(); } chacha = null; } case "chacha-aead" -> { - chacha = ChaChaDataContentBuilder.builder(); + chacha = ChaChaDataContentBuilder.builder(session); // selecting AEAD: if the user did not supply AAD, pass empty to pick AEAD chacha.withAad(aad != null ? aad : new byte[0]); if (header) { @@ -302,7 +318,7 @@ public final class Guard { aes = null; } case "chacha-stream" -> { - chacha = ChaChaDataContentBuilder.builder(); + chacha = ChaChaDataContentBuilder.builder(session); if (header) { chacha.withHeader(); } @@ -321,71 +337,93 @@ public final class Guard { } // envelope builder (new API) - final MultiRecipientDataSourceBuilder env = new MultiRecipientDataSourceBuilder().payloadKeyBytes(cekBytes) + final MultiRecipientDataSourceBuilder env = MultiRecipientDataSourceBuilder.builder(session) + .payloadKeyBytes(cekBytes) .headerLimits(maxRecipients, maxEntryLen); - if (aes != null) { - env.withAes(aes); - } else { - env.withChaCha(chacha); - } - // shuffle on by default - if (shuffle) { - env.shuffle(); - } - // recipients and decoys only apply on encrypt - if (encrypt) { - final int iter = Integer.parseInt(cmd.getOptionValue(OPT_PSW_ITER, "200000")); - final int saltLen = Integer.parseInt(cmd.getOptionValue(OPT_PSW_SALT, "16")); - final int kekLen = Integer.parseInt(cmd.getOptionValue(OPT_PSW_KEK, "32")); - - final KeyringStore ks = loadKeyringIfPresent(cmd, OPT_KEYRING); - // real recipients by alias - for (String alias : cmd.getOptionValues(OPT_TO_ALIAS) == null ? new String[0] - : cmd.getOptionValues(OPT_TO_ALIAS)) { - addRecipientFromAlias(env, ks, alias, kekLen, saltLen, false); - } - // real password recipients - for (String psw : cmd.getOptionValues(OPT_TO_PSW) == null ? new String[0] - : cmd.getOptionValues(OPT_TO_PSW)) { - env.addPasswordRecipient(psw.toCharArray(), iter, saltLen, kekLen); - } - // decoys by alias (key types) - for (String alias : cmd.getOptionValues(OPT_DECOY_ALIAS) == null ? new String[0] - : cmd.getOptionValues(OPT_DECOY_ALIAS)) { - addRecipientFromAlias(env, ks, alias, kekLen, saltLen, true); - } - // decoy passwords (explicit) - for (String psw : cmd.getOptionValues(OPT_DECOY_PSW) == null ? new String[0] - : cmd.getOptionValues(OPT_DECOY_PSW)) { - env.addPasswordRecipientDecoy(psw.toCharArray(), iter, saltLen, kekLen); - } - // decoy passwords (random) - final int rndCount = Integer.parseInt(cmd.getOptionValue(OPT_DECOY_PSW_RAND, "0")); - for (int i = 0; i < rndCount; i++) { - env.addPasswordRecipientDecoy(randomPassword(), iter, saltLen, kekLen); - } - } else { - // unlock material for decrypt - final String privAlias = cmd.getOptionValue(OPT_PRIV_ALIAS); - final String password = cmd.getOptionValue(OPT_PASSWORD); - - if ((privAlias == null && password == null) || (privAlias != null && password != null)) { - throw new ParseException("Specify exactly one of --priv-alias or --password for decryption"); - } - if (privAlias != null) { - final KeyringStore ks = requireKeyring(cmd, OPT_KEYRING); - final KeyringStore.PrivateWithId pr = ks.getPrivateWithId(privAlias); - env.unlockWith(new UnlockMaterial.Private(pr.key())); + UnlockMaterial borrowedUnlockMaterial = null; + try (env) { + if (aes != null) { + env.withAes(aes); } else { - env.unlockWith(new UnlockMaterial.Password(password.toCharArray())); + env.withChaCha(chacha); } - } + if (shuffle) { + env.shuffle(); + } + if (encrypt) { + final int iter = Integer.parseInt(cmd.getOptionValue(OPT_PSW_ITER, "200000")); + final int saltLen = Integer.parseInt(cmd.getOptionValue(OPT_PSW_SALT, "16")); + final int kekLen = RecipientKekSizes.requireSupported( + Integer.parseInt(cmd.getOptionValue(OPT_PSW_KEK, "32"))); - // connect upstream and run - final DataContent content = env.build(encrypt); // env installs default openers on decrypt if none were added - content.setInput(PlainFileBuilder.builder().url(inPath.toUri().toURL()).build(encrypt)); - try (InputStream in = content.getStream(); OutputStream out = Files.newOutputStream(outPath)) { - in.transferTo(out); + final KeyringStore ks = loadKeyringIfPresent(session, cmd, OPT_KEYRING); + for (String alias : cmd.getOptionValues(OPT_TO_ALIAS) == null ? new String[0] + : cmd.getOptionValues(OPT_TO_ALIAS)) { + addRecipientFromAlias(session, env, ks, alias, kekLen, saltLen, false); + } + for (String psw : cmd.getOptionValues(OPT_TO_PSW) == null ? new String[0] + : cmd.getOptionValues(OPT_TO_PSW)) { + char[] passwordChars = psw.toCharArray(); + try { + env.addPasswordRecipient(passwordChars, iter, saltLen, kekLen); + } finally { + Arrays.fill(passwordChars, '\0'); + } + } + for (String alias : cmd.getOptionValues(OPT_DECOY_ALIAS) == null ? new String[0] + : cmd.getOptionValues(OPT_DECOY_ALIAS)) { + addRecipientFromAlias(session, env, ks, alias, kekLen, saltLen, true); + } + for (String psw : cmd.getOptionValues(OPT_DECOY_PSW) == null ? new String[0] + : cmd.getOptionValues(OPT_DECOY_PSW)) { + char[] passwordChars = psw.toCharArray(); + try { + env.addPasswordRecipientDecoy(passwordChars, iter, saltLen, kekLen); + } finally { + Arrays.fill(passwordChars, '\0'); + } + } + final int rndCount = Integer.parseInt(cmd.getOptionValue(OPT_DECOY_PSW_RAND, "0")); + for (int i = 0; i < rndCount; i++) { + char[] passwordChars = randomPassword(); + try { + env.addPasswordRecipientDecoy(passwordChars, iter, saltLen, kekLen); + } finally { + Arrays.fill(passwordChars, '\0'); + } + } + } else { + final String privAlias = cmd.getOptionValue(OPT_PRIV_ALIAS); + final String password = cmd.getOptionValue(OPT_PASSWORD); + + if ((privAlias == null && password == null) || (privAlias != null && password != null)) { + throw new ParseException("Specify exactly one of --priv-alias or --password for decryption"); + } + if (privAlias != null) { + final KeyringStore ks = requireKeyring(session, cmd, OPT_KEYRING); + final KeyringStore.PrivateWithId pr = ks.getPrivateWithId(privAlias); + borrowedUnlockMaterial = new UnlockMaterial.Private(pr.key()); + } else { + char[] passwordChars = password.toCharArray(); + try { + borrowedUnlockMaterial = new UnlockMaterial.Password(passwordChars); + } finally { + Arrays.fill(passwordChars, '\0'); + } + } + env.unlockWith(borrowedUnlockMaterial); + } + + try (MultiRecipientContent content = env.build(encrypt)) { + content.setInput(PlainFileBuilder.builder().url(inPath.toUri().toURL()).build(encrypt)); + try (InputStream in = content.getStream(); OutputStream out = Files.newOutputStream(outPath)) { + in.transferTo(out); + } + } + } finally { + if (borrowedUnlockMaterial instanceof UnlockMaterial.Password passwordMaterial) { + passwordMaterial.destroy(); + } } return 0; @@ -404,6 +442,33 @@ public final class Guard { return Paths.get(s + suffix); } + private static ZeroEchoSession createSession(CommandLine cmd, Option operationalMaximumOption, + Option absoluteMaximumOption, boolean passwordOperation) throws ParseException { + boolean hasOperationalMaximum = cmd.hasOption(operationalMaximumOption); + boolean hasAbsoluteMaximum = cmd.hasOption(absoluteMaximumOption); + if (!hasOperationalMaximum && !hasAbsoluteMaximum) { + if (passwordOperation) { + throw new ParseException( + "Password operations require --pbkdf2-max and --pbkdf2-hard-max"); + } + return new ZeroEchoSession(); + } + if (!hasOperationalMaximum || !hasAbsoluteMaximum) { + throw new ParseException("--pbkdf2-max and --pbkdf2-hard-max must be specified together"); + } + try { + int operationalMaximum = Integer.parseInt(cmd.getOptionValue(operationalMaximumOption)); + int absoluteMaximum = Integer.parseInt(cmd.getOptionValue(absoluteMaximumOption)); + return new ZeroEchoSession().withPbkdf2Limits( + new Pbkdf2Limits(operationalMaximum, absoluteMaximum)); + } catch (IllegalArgumentException exception) { + ParseException parseException = + new ParseException("Invalid PBKDF2 limits: " + exception.getMessage()); + parseException.initCause(exception); + throw parseException; + } + } + private static byte[] parseHex(String s) throws ParseException { try { return HexFormat.of().parseHex(s); @@ -434,15 +499,14 @@ public final class Guard { * *

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

* * @param env target builder to which the recipient is added * @param ks keyring store that provides public keys by alias * @param alias alias name of the recipient's public key in the keyring - * @param kekBytes desired length in bytes of the key-encryption key when using - * KEM + * @param kekBytes key-encryption key length; exactly 16 or 32 bytes * @param saltLen salt length in bytes when using KEM * @param decoy whether the recipient is a decoy * @throws GeneralSecurityException if the algorithm does not support the @@ -450,29 +514,52 @@ public final class Guard { * @throws IOException if context creation or builder operations * require I/O and fail */ - private static void addRecipientFromAlias(MultiRecipientDataSourceBuilder env, KeyringStore ks, String alias, + @SuppressWarnings({ "PMD.CloseResource", "PMD.UseTryWithResources" }) + private static void addRecipientFromAlias(ZeroEchoSession session, MultiRecipientDataSourceBuilder env, + KeyringStore ks, String alias, int kekBytes, int saltLen, boolean decoy) throws GeneralSecurityException, IOException { KeyringStore.PublicWithId r = ks.getPublicWithId(alias); final String algId = r.algorithm(); final java.security.PublicKey pub = r.key(); - // Try KEM first - try (KemContext kem = CryptoAlgorithms.create(algId, KeyUsage.ENCAPSULATE, pub)) { - if (decoy) { - env.addRecipientDecoy(kem, kekBytes, saltLen); // builder closes context - } else { - env.addRecipient(kem, kekBytes, saltLen); // builder closes context - } + KemContext kem; + try { + kem = session.createContext(algId, KeyUsage.ENCAPSULATE, pub); + } catch (UnsupportedRoleException notKem) { + addEncryptionRecipient(session, env, algId, pub, decoy); return; - } catch (Exception notKem) { // NOPMD - // fall back to public-key encryption } - try (EncryptionContext enc = CryptoAlgorithms.create(algId, KeyUsage.ENCRYPT, pub)) { + boolean transferred = false; + try { if (decoy) { - env.addRecipientDecoy(enc); // builder closes context + env.addRecipientDecoy(kem, kekBytes, saltLen); } else { - env.addRecipient(enc); // builder closes context + env.addRecipient(kem, kekBytes, saltLen); + } + transferred = true; + } finally { + if (!transferred) { + kem.close(); + } + } + } + + @SuppressWarnings({ "PMD.CloseResource", "PMD.UseTryWithResources" }) + private static void addEncryptionRecipient(ZeroEchoSession session, MultiRecipientDataSourceBuilder env, + String algorithmId, java.security.PublicKey publicKey, boolean decoy) throws IOException { + EncryptionContext encryption = session.createContext(algorithmId, KeyUsage.ENCRYPT, publicKey); + boolean transferred = false; + try { + if (decoy) { + env.addRecipientDecoy(encryption); + } else { + env.addRecipient(encryption); + } + transferred = true; + } finally { + if (!transferred) { + encryption.close(); } } } @@ -480,26 +567,27 @@ public final class Guard { private static char[] randomPassword() { // simple random alnum for decoy purposes only final String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - final SecureRandom rnd = new SecureRandom(); - final int len = 16 + rnd.nextInt(17); // 16..32 + final int len = 16 + RandomSupport.nextInt(17); // 16..32 final char[] out = new char[len]; for (int i = 0; i < len; i++) { - out[i] = alphabet.charAt(rnd.nextInt(alphabet.length())); + out[i] = alphabet.charAt(RandomSupport.nextInt(alphabet.length())); } return out; } - private static KeyringStore loadKeyringIfPresent(CommandLine cmd, Option optKs) throws IOException { + private static KeyringStore loadKeyringIfPresent(ZeroEchoSession session, CommandLine cmd, Option optKs) + throws IOException { if (!cmd.hasOption(optKs)) { - return new KeyringStore(); + return new KeyringStore(session); } - return KeyringStore.load(Paths.get(cmd.getOptionValue(optKs))); + return KeyringStore.load(session, Paths.get(cmd.getOptionValue(optKs))); } - private static KeyringStore requireKeyring(CommandLine cmd, Option optKs) throws IOException, ParseException { + private static KeyringStore requireKeyring(ZeroEchoSession session, CommandLine cmd, Option optKs) + throws IOException, ParseException { if (!cmd.hasOption(optKs)) { throw new ParseException("--keyring is required when aliases are used"); } - return KeyringStore.load(Paths.get(cmd.getOptionValue(optKs))); + return KeyringStore.load(session, Paths.get(cmd.getOptionValue(optKs))); } } diff --git a/app/src/main/java/zeroecho/Kem.java b/app/src/main/java/zeroecho/Kem.java index 85d954d..4ed2b24 100644 --- a/app/src/main/java/zeroecho/Kem.java +++ b/app/src/main/java/zeroecho/Kem.java @@ -63,6 +63,7 @@ import zeroecho.core.storage.KeyringStore; import zeroecho.sdk.builders.alg.AesDataContentBuilder; import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder; import zeroecho.sdk.builders.alg.KemDataContentBuilder; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.sdk.builders.core.DataContentChainBuilder; import zeroecho.sdk.builders.core.PlainFileBuilder; import zeroecho.sdk.content.api.DataContent; @@ -203,6 +204,7 @@ public final class Kem { // NOPMD } public static int main(String[] args, Options opts) throws ParseException, IOException, GeneralSecurityException { // NOPMD + ZeroEchoSession session = new ZeroEchoSession(); defineOptions(opts); CommandLineParser parser = new DefaultParser(); CommandLine cmd = parser.parse(opts, args); @@ -240,10 +242,10 @@ public final class Kem { // NOPMD final String kemId = cmd.getOptionValue(OPT_KEM.getLongOpt()); final Path keyringPath = Path.of(cmd.getOptionValue(OPT_KEYRING.getLongOpt())); - final KeyringStore keyring = KeyringStore.load(keyringPath); + final KeyringStore keyring = KeyringStore.load(session, keyringPath); // Configure KEM envelope - KemDataContentBuilder kem = KemDataContentBuilder.builder().kem(kemId); + KemDataContentBuilder kem = KemDataContentBuilder.builder(session).kem(kemId); if (cmd.hasOption(OPT_DIRECT.getLongOpt())) { kem = kem.directSecret(); } else { @@ -267,7 +269,7 @@ public final class Kem { // NOPMD // AES payload if (wantAes) { String mode = cmd.getOptionValue(OPT_AES_CIPHER.getLongOpt(), "gcm").toLowerCase(Locale.ROOT); - AesDataContentBuilder aes = AesDataContentBuilder.builder(); + AesDataContentBuilder aes = AesDataContentBuilder.builder(session); switch (mode) { case "gcm" -> { Integer tagBitsOpt = parsedIntOpt(cmd, OPT_AES_TAG_BITS); @@ -294,7 +296,7 @@ public final class Kem { // NOPMD // ChaCha payload if (wantChaCha) { - ChaChaDataContentBuilder cc = ChaChaDataContentBuilder.builder(); + ChaChaDataContentBuilder cc = ChaChaDataContentBuilder.builder(session); byte[] nonce = parseHexOpt(cmd, OPT_CHACHA_NONCE); if (nonce != null) { cc = cc.withNonce(nonce); diff --git a/app/src/main/java/zeroecho/KeyStoreManagement.java b/app/src/main/java/zeroecho/KeyStoreManagement.java index e74df90..5e700b8 100644 --- a/app/src/main/java/zeroecho/KeyStoreManagement.java +++ b/app/src/main/java/zeroecho/KeyStoreManagement.java @@ -42,6 +42,7 @@ import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.security.GeneralSecurityException; import java.security.KeyPair; import java.util.ArrayList; import java.util.Base64; @@ -61,10 +62,11 @@ import org.apache.commons.cli.ParseException; import zeroecho.core.CryptoAlgorithm; import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyOperation; +import zeroecho.core.KeyOperationInfo; import zeroecho.core.spec.AlgorithmKeySpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; -import zeroecho.core.spi.SymmetricKeyBuilder; import zeroecho.core.storage.KeyringStore; +import zeroecho.sdk.ZeroEchoSession; /** * Command-line utility for managing key material in a text-based keyring store. @@ -231,6 +233,7 @@ public final class KeyStoreManagement { // NOPMD * @throws IOException if I/O fails */ public static int main(final String[] args, final Options dispatcherOptions) throws ParseException, IOException { + ZeroEchoSession session = new ZeroEchoSession(); defineOptions(dispatcherOptions); CommandLineParser parser = new DefaultParser(); CommandLine cmd = parser.parse(dispatcherOptions, args); @@ -245,14 +248,15 @@ public final class KeyStoreManagement { // NOPMD } Path keyringPath = Path.of(cmd.getOptionValue(KEYSTORE_OPTION.getLongOpt())); - KeyringStore store = Files.exists(keyringPath) ? KeyringStore.load(keyringPath) : new KeyringStore(); + KeyringStore store = Files.exists(keyringPath) ? KeyringStore.load(session, keyringPath) + : new KeyringStore(session); if (cmd.hasOption(LIST_ALIASES_OPTION.getLongOpt())) { listAliases(store); return 0; } if (cmd.hasOption(GENERATE_OPTION.getLongOpt())) { - doGenerate(store, cmd); + doGenerate(session, store, cmd); store.save(keyringPath); return 0; } @@ -311,8 +315,12 @@ public final class KeyStoreManagement { // NOPMD Set ids = CryptoAlgorithms.available(); for (String id : ids) { CryptoAlgorithm a = CryptoAlgorithms.require(id); - boolean hasAsym = !a.asymmetricBuildersInfo().isEmpty(); - boolean hasSym = !a.symmetricBuildersInfo().isEmpty(); + boolean hasAsym = a.keyOperations().stream() + .anyMatch(info -> info.operation() != KeyOperation.SYMMETRIC_GENERATE + && info.operation() != KeyOperation.SYMMETRIC_IMPORT); + boolean hasSym = a.keyOperations().stream() + .anyMatch(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE + || info.operation() == KeyOperation.SYMMETRIC_IMPORT); out.printf(Locale.ROOT, "%-12s asym:%s sym:%s%n", id, hasAsym, hasSym); } } @@ -346,7 +354,8 @@ public final class KeyStoreManagement { // NOPMD * @param store keyring store to mutate * @param cmd parsed command line */ - public static void doGenerate(final KeyringStore store, final CommandLine cmd) { // NOPMD + public static void doGenerate(final ZeroEchoSession session, final KeyringStore store, + final CommandLine cmd) { String algId = required(cmd, ALG_OPTION, "--alg is required for --generate"); String aliasBase = required(cmd, ALIAS_OPTION, "--alias is required for --generate"); String kind = cmd.getOptionValue(KIND_OPTION.getLongOpt()); @@ -355,119 +364,155 @@ public final class KeyStoreManagement { // NOPMD boolean overwrite = cmd.hasOption(OVERWRITE_OPTION.getLongOpt()); CryptoAlgorithm alg = CryptoAlgorithms.require(algId); - boolean canAsym = !alg.asymmetricBuildersInfo().isEmpty(); - boolean canSym = !alg.symmetricBuildersInfo().isEmpty(); + boolean canAsym = alg.keyOperations().stream() + .anyMatch(info -> info.operation() == KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE); + boolean canSym = alg.keyOperations().stream() + .anyMatch(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE); - boolean doAsym = "asym".equalsIgnoreCase(kind) || (kind == null && canAsym && !canSym); - boolean doSym = "sym".equalsIgnoreCase(kind) || (kind == null && canSym && !canAsym); + GenerationKind generationKind = selectGenerationKind(kind, canAsym, canSym); + if (generationKind == GenerationKind.ASYMMETRIC) { + generateAsymmetric(session, store, alg, algId, aliasBase, pubSfx, prvSfx, overwrite); + } + if (generationKind == GenerationKind.SYMMETRIC) { + generateSymmetric(session, store, alg, algId, aliasBase, overwrite); + } + } - if (!doAsym && !doSym && canAsym && canSym) { + private static GenerationKind selectGenerationKind(String requestedKind, boolean canAsymmetric, + boolean canSymmetric) { + if ("asym".equalsIgnoreCase(requestedKind) || requestedKind == null && canAsymmetric && !canSymmetric) { + return GenerationKind.ASYMMETRIC; + } + if ("sym".equalsIgnoreCase(requestedKind) || requestedKind == null && canSymmetric && !canAsymmetric) { + return GenerationKind.SYMMETRIC; + } + if (canAsymmetric && canSymmetric) { throw new IllegalArgumentException("Algorithm supports both; specify --kind sym|asym"); } + return GenerationKind.NONE; + } - if (doAsym) { - KeyPair kp = null; - CryptoAlgorithm.AsymBuilderInfo used = null; - for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) { - if (bi.defaultKeySpec == null) { - continue; - } - @SuppressWarnings("unchecked") - Class st = (Class) bi.specType; - AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(st); - try { - kp = b.generateKeyPair((AlgorithmKeySpec) bi.defaultKeySpec); - if (kp != null) { - used = bi; - break; - } - } catch (Throwable ignore) { // NOPMD - } - } - if (kp == null || used == null) { - throw new IllegalStateException("No asymmetric builder with default spec worked for " + algId); - } - - Class pubImp = null; - Class prvImp = null; - for (CryptoAlgorithm.AsymBuilderInfo x : alg.asymmetricBuildersInfo()) { - if (looksLikeImportSpecForPublic(x.specType)) { - pubImp = x.specType; - } - if (looksLikeImportSpecForPrivate(x.specType)) { - prvImp = x.specType; - } - } - if (pubImp == null && prvImp == null) { - throw new IllegalStateException("No import spec class found for " + algId + " (asymmetric)"); - } - - byte[] spki = kp.getPublic() != null ? kp.getPublic().getEncoded() : null; - byte[] pkcs8 = kp.getPrivate() != null ? kp.getPrivate().getEncoded() : null; - - AlgorithmKeySpec pubSpec = pubImp != null ? makeImportSpec(pubImp, spki, algId, used.defaultKeySpec) : null; - AlgorithmKeySpec prvSpec = prvImp != null ? makeImportSpec(prvImp, pkcs8, algId, used.defaultKeySpec) - : null; - - if (pubImp != null && pubSpec == null) { - throw new IllegalStateException("Cannot construct public import spec for " + algId); - } - if (prvImp != null && prvSpec == null) { - throw new IllegalStateException("Cannot construct private import spec for " + algId); - } - - String pubAlias = aliasBase + pubSfx; - String prvAlias = aliasBase + prvSfx; - ensureWritable(store, pubAlias, overwrite); - ensureWritable(store, prvAlias, overwrite); - - store.putPublic(pubAlias, algId, pubSpec); - store.putPrivate(prvAlias, algId, prvSpec); - - PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD - out.printf("Generated %s -> %s, %s%n", algId, pubAlias, prvAlias); + private static void generateAsymmetric(ZeroEchoSession session, KeyringStore store, CryptoAlgorithm algorithm, + String algorithmId, String aliasBase, String publicSuffix, String privateSuffix, boolean overwrite) { + GeneratedKeyPair generated = firstGeneratedKeyPair(session, algorithm, algorithmId); + Class publicImport = findImportSpecClass(algorithm, KeyOperation.ASYMMETRIC_PUBLIC_IMPORT, true); + Class privateImport = findImportSpecClass(algorithm, KeyOperation.ASYMMETRIC_PRIVATE_IMPORT, false); + if (publicImport == null && privateImport == null) { + throw new IllegalStateException("No import spec class found for " + algorithmId + " (asymmetric)"); } - if (doSym) { - SecretKey sk = null; - CryptoAlgorithm.SymBuilderInfo used = null; - for (CryptoAlgorithm.SymBuilderInfo bi : alg.symmetricBuildersInfo()) { - if (bi.defaultKeySpec() == null) { - continue; - } - @SuppressWarnings("unchecked") - Class st = (Class) bi.specType(); - SymmetricKeyBuilder b = alg.symmetricKeyBuilder(st); - try { - sk = b.generateSecret((AlgorithmKeySpec) bi.defaultKeySpec()); - if (sk != null) { - used = bi; - break; - } - } catch (Throwable ignore) { // NOPMD + KeyPair pair = generated.pair(); + byte[] publicEncoding = pair.getPublic() == null ? null : pair.getPublic().getEncoded(); + byte[] privateEncoding = pair.getPrivate() == null ? null : pair.getPrivate().getEncoded(); + AlgorithmKeySpec publicSpec = publicImport == null ? null + : makeImportSpec(publicImport, publicEncoding, algorithmId, generated.info().defaultSpec()); + AlgorithmKeySpec privateSpec = privateImport == null ? null + : makeImportSpec(privateImport, privateEncoding, algorithmId, generated.info().defaultSpec()); + requireImportSpec(publicImport, publicSpec, "public", algorithmId); + requireImportSpec(privateImport, privateSpec, "private", algorithmId); + + String publicAlias = aliasBase + publicSuffix; + String privateAlias = aliasBase + privateSuffix; + ensureWritable(store, publicAlias, overwrite); + ensureWritable(store, privateAlias, overwrite); + store.putPublic(publicAlias, algorithmId, publicSpec); + store.putPrivate(privateAlias, algorithmId, privateSpec); + + PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD + out.printf("Generated %s -> %s, %s%n", algorithmId, publicAlias, privateAlias); + } + + private static GeneratedKeyPair firstGeneratedKeyPair(ZeroEchoSession session, CryptoAlgorithm algorithm, + String algorithmId) { + for (KeyOperationInfo info : algorithm.keyOperations()) { + if (info.operation() != KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE || info.defaultSpec() == null) { + continue; + } + @SuppressWarnings("unchecked") + Class specType = (Class) info.specType(); + try { + KeyPair pair = session.keyBuilders().asymmetric().keyPairGenerator(algorithmId, specType) + .generateKeyPair(info.defaultSpec()); + if (pair != null) { + return new GeneratedKeyPair(pair, info); } + } catch (GeneralSecurityException | RuntimeException ignored) { // NOPMD + // Try the next registered default specification. } - if (sk == null || used == null) { - throw new IllegalStateException("No symmetric builder with default spec worked for " + algId); - } - - Class impSym = findSymmetricImportSpecClass(alg); - if (impSym == null) { - throw new IllegalStateException("No symmetric import spec class for " + algId); - } - - byte[] raw = sk.getEncoded(); - AlgorithmKeySpec secSpec = makeImportSpec(impSym, raw, algId, used.defaultKeySpec()); - if (secSpec == null) { - throw new IllegalStateException("Cannot construct symmetric import spec for " + algId); - } - - ensureWritable(store, aliasBase, overwrite); - store.putSecret(aliasBase, algId, secSpec); - - PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD - out.printf("Generated %s -> %s%n", algId, aliasBase); } + throw new IllegalStateException("No asymmetric builder with default spec worked for " + algorithmId); + } + + private static Class findImportSpecClass(CryptoAlgorithm algorithm, KeyOperation operation, + boolean publicImport) { + for (KeyOperationInfo info : algorithm.keyOperations()) { + boolean matchingName = publicImport ? looksLikeImportSpecForPublic(info.specType()) + : looksLikeImportSpecForPrivate(info.specType()); + if (info.operation() == operation && matchingName) { + return info.specType(); + } + } + return null; + } + + private static void requireImportSpec(Class specType, AlgorithmKeySpec spec, String kind, String algorithmId) { + if (specType != null && spec == null) { + throw new IllegalStateException("Cannot construct " + kind + " import spec for " + algorithmId); + } + } + + private static void generateSymmetric(ZeroEchoSession session, KeyringStore store, CryptoAlgorithm algorithm, + String algorithmId, String alias, boolean overwrite) { + GeneratedSecret generated = firstGeneratedSecret(session, algorithm, algorithmId); + Class importType = findSymmetricImportSpecClass(algorithm); + if (importType == null) { + throw new IllegalStateException("No symmetric import spec class for " + algorithmId); + } + + byte[] encoding = generated.key().getEncoded(); + AlgorithmKeySpec spec = makeImportSpec(importType, encoding, algorithmId, generated.info().defaultSpec()); + if (spec == null) { + throw new IllegalStateException("Cannot construct symmetric import spec for " + algorithmId); + } + ensureWritable(store, alias, overwrite); + store.putSecret(alias, algorithmId, spec); + + PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD + out.printf("Generated %s -> %s%n", algorithmId, alias); + } + + private static GeneratedSecret firstGeneratedSecret(ZeroEchoSession session, CryptoAlgorithm algorithm, + String algorithmId) { + for (KeyOperationInfo info : algorithm.keyOperations()) { + if (info.operation() != KeyOperation.SYMMETRIC_GENERATE || info.defaultSpec() == null) { + continue; + } + @SuppressWarnings("unchecked") + Class specType = (Class) info.specType(); + try { + SecretKey key = session.keyBuilders().symmetric().generator(algorithmId, specType) + .generateSecret(info.defaultSpec()); + if (key != null) { + return new GeneratedSecret(key, info); + } + } catch (GeneralSecurityException | RuntimeException ignored) { // NOPMD + // Try the next registered default specification. + } + } + throw new IllegalStateException("No symmetric builder with default spec worked for " + algorithmId); + } + + private record GeneratedKeyPair(KeyPair pair, KeyOperationInfo info) { + } + + private record GeneratedSecret(SecretKey key, KeyOperationInfo info) { + } + + /** Selects the exact key-generation operation requested by the command. */ + private enum GenerationKind { + ASYMMETRIC, + SYMMETRIC, + NONE } /** @@ -545,19 +590,11 @@ public final class KeyStoreManagement { // NOPMD } private static Class findSymmetricImportSpecClass(CryptoAlgorithm alg) { - for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) { - String n = x.specType().getSimpleName(); - if (n.contains("Import") || n.endsWith("KeyImportSpec") || n.endsWith("SecretSpec")) { + for (KeyOperationInfo x : alg.keyOperations()) { + if (x.operation() == KeyOperation.SYMMETRIC_IMPORT) { return x.specType(); } } - for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) { - try { - x.specType().getConstructor(byte[].class); - return x.specType(); - } catch (NoSuchMethodException ignored) { - } - } return null; } diff --git a/app/src/main/java/zeroecho/Tag.java b/app/src/main/java/zeroecho/Tag.java index 05b746e..fb1be9e 100644 --- a/app/src/main/java/zeroecho/Tag.java +++ b/app/src/main/java/zeroecho/Tag.java @@ -59,6 +59,7 @@ import zeroecho.core.spec.VoidSpec; import zeroecho.core.storage.KeyringStore; import zeroecho.core.tag.TagEngineBuilder; import zeroecho.sdk.builders.TagTrailerDataContentBuilder; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.sdk.content.api.DataContent; import zeroecho.sdk.content.api.PlainContent; import zeroecho.sdk.content.builtin.PlainFile; @@ -157,6 +158,7 @@ public final class Tag { // NOPMD * signature or digest processing */ public static int main(String[] args, Options root) throws ParseException, IOException, GeneralSecurityException { + ZeroEchoSession session = new ZeroEchoSession(); Options opts = root; opts.addOption(TYPE_OPT); opts.addOption(MODE_OPT); @@ -196,21 +198,23 @@ public final class Tag { // NOPMD if (TYPE_SIGNATURE.equals(type)) { String ksPath = require(cli, KS_OPT, "--ks is required for --type signature"); - KeyringStore keyring = KeyringStore.load(Path.of(ksPath)); + KeyringStore keyring = KeyringStore.load(session, Path.of(ksPath)); ContextSpec spec = VoidSpec.INSTANCE; if (produce) { String privAlias = require(cli, PRIV_OPT, "signature produce requires --priv "); PrivateKey priv = keyring.getPrivate(privAlias); - tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(alg, priv, spec)).build(true); + tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(session, alg, priv, spec)) + .build(true); } else { String pubAlias = require(cli, PUB_OPT, "signature verify requires --pub "); PublicKey pub = keyring.getPublic(pubAlias); - tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(alg, pub, spec)).build(false); + tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(session, alg, pub, spec)) + .build(false); } } else { // digest DigestSpec spec = parseDigest(alg); - tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(spec)).build(produce); + tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(session, spec)).build(produce); } tail.setInput(source); diff --git a/app/src/test/java/zeroecho/GuardTest.java b/app/src/test/java/zeroecho/GuardTest.java index fd90fb2..59436aa 100644 --- a/app/src/test/java/zeroecho/GuardTest.java +++ b/app/src/test/java/zeroecho/GuardTest.java @@ -36,6 +36,7 @@ package zeroecho; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.PrintStream; import java.nio.file.Files; @@ -45,6 +46,7 @@ import java.util.Arrays; import java.util.Random; import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -74,6 +76,7 @@ import zeroecho.sdk.util.BouncyCastleActivator; *

*/ public class GuardTest { + private static final String TEST_PBKDF2_MAXIMUM = "1000000"; /** All temporary files live here and are auto-cleaned by JUnit. */ @TempDir @@ -120,14 +123,16 @@ public class GuardTest { Path dec = tmp.resolve("pt.bin.dec"); // Encrypt - String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", password, "--alg", + String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", password, + "--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg", "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aadHex }; System.out.println("...encrypt: " + Arrays.toString(encArgs)); int e = Guard.main(encArgs, new Options()); assertEquals(0, e, "... encrypt expected exit code 0"); // Decrypt (using password) - String[] decArgs = { "--decrypt", enc.toString(), "--output", dec.toString(), "--password", password, "--alg", + String[] decArgs = { "--decrypt", enc.toString(), "--output", dec.toString(), "--password", password, + "--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg", "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aadHex }; System.out.println("...decrypt: " + Arrays.toString(decArgs)); int d = Guard.main(decArgs, new Options()); @@ -137,6 +142,42 @@ public class GuardTest { System.out.println("...ok"); } + @Test + void passwordOperationRequiresExplicitLimits() throws Exception { + String method = "passwordOperationRequiresExplicitLimits"; + System.out.println(method); + Path input = writeRandom(tmp.resolve("limits.bin"), 32, 0x51A17); + String[] arguments = { "--encrypt", input.toString(), "--to-psw", "controlled", "--alg", "aes-gcm" }; + + ParseException failure = assertThrows(ParseException.class, + () -> Guard.main(arguments, new Options())); + + assertTrue(failure.getMessage().contains("--pbkdf2-max")); + System.out.println("...rejected=missingLimits"); + System.out.println(method + "...ok"); + } + + @Test + void recipientKekOptionRejectsUnreadableSize() throws Exception { + String method = "recipientKekOptionRejectsUnreadableSize"; + System.out.println(method); + Path input = writeRandom(tmp.resolve("invalid-kek.bin"), 32, 0x4B454B); + Path output = tmp.resolve("invalid-kek.enc"); + String[] arguments = { "--encrypt", input.toString(), "--output", output.toString(), + "--to-psw", "controlled", "--to-kek-bytes", "24", + "--pbkdf2-max", TEST_PBKDF2_MAXIMUM, + "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, + "--alg", "aes-gcm" }; + + IllegalArgumentException failure = assertThrows(IllegalArgumentException.class, + () -> Guard.main(arguments, new Options())); + + assertTrue(failure.getMessage().contains("exactly 16 or 32")); + assertTrue(Files.notExists(output)); + System.out.println("...rejectedKekBytes=24"); + System.out.println(method + "...ok"); + } + /** * RSA recipient round trips with both AES-GCM and ChaCha20-Poly1305 payloads. * @@ -249,7 +290,8 @@ public class GuardTest { // alias, // plus 2 random password decoys. Recipients are shuffled by default. String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--keyring", ring.toString(), - "--to-alias", rsa.pub, "--to-psw", password, "--decoy-alias", elg.pub, "--decoy-psw-rand", "2", "--alg", + "--to-alias", rsa.pub, "--to-psw", password, "--decoy-alias", elg.pub, "--decoy-psw-rand", "2", + "--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg", "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aad }; System.out.println("...encrypt: " + Arrays.toString(encArgs)); int e = Guard.main(encArgs, new Options()); @@ -266,7 +308,8 @@ public class GuardTest { "mixed recipients decrypt(private) mismatch"); // Decrypt via password instead of key - String[] decPwd = { "--decrypt", enc.toString(), "--output", dec2.toString(), "--password", password, "--alg", + String[] decPwd = { "--decrypt", enc.toString(), "--output", dec2.toString(), "--password", password, + "--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg", "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aad }; System.out.println("...decrypt(password): " + Arrays.toString(decPwd)); int d2 = Guard.main(decPwd, new Options()); @@ -292,7 +335,8 @@ public class GuardTest { Path enc = tmp.resolve("pt-neg.bin.enc"); String pwd = "x"; - String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", pwd, "--alg", + String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", pwd, + "--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg", "aes-gcm", "--tag-bits", "128" }; int e = Guard.main(encArgs, new Options()); assertEquals(0, e, "... encrypt rc"); diff --git a/app/src/test/java/zeroecho/KemTest.java b/app/src/test/java/zeroecho/KemTest.java index 0b4da56..8f97297 100644 --- a/app/src/test/java/zeroecho/KemTest.java +++ b/app/src/test/java/zeroecho/KemTest.java @@ -166,7 +166,7 @@ public class KemTest { KeyAliases aliases = generateKemIntoKeyStore(ring, kemId, "alias-" + shortId(kemId)); // Sanity: re-open to ensure the file is valid - KeyringStore ks = KeyringStore.load(ring); + KeyringStore ks = KeyringStore.load(new zeroecho.sdk.ZeroEchoSession(), ring); if (!(ks.contains(aliases.pub) && ks.contains(aliases.prv))) { throw new IllegalStateException("Keyring does not contain expected aliases for " + kemId); } diff --git a/app/src/test/java/zeroecho/KeyStoreManagementTest.java b/app/src/test/java/zeroecho/KeyStoreManagementTest.java index 67ed879..ebf540a 100644 --- a/app/src/test/java/zeroecho/KeyStoreManagementTest.java +++ b/app/src/test/java/zeroecho/KeyStoreManagementTest.java @@ -52,10 +52,9 @@ import org.junit.jupiter.api.io.TempDir; import zeroecho.core.CryptoAlgorithm; import zeroecho.core.CryptoAlgorithms; -import zeroecho.core.spec.AlgorithmKeySpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; -import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.core.KeyOperation; import zeroecho.core.storage.KeyringStore; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.sdk.util.BouncyCastleActivator; /** @@ -91,6 +90,7 @@ public class KeyStoreManagementTest { @Test public void generateAndVerifyAllAlgorithms() throws Exception { Path ring = tmp.resolve("ring.txt"); + ZeroEchoSession session = new ZeroEchoSession(); Set algIds = CryptoAlgorithms.available(); System.out.println("Algorithms: " + algIds); @@ -139,7 +139,7 @@ public class KeyStoreManagementTest { assertTrue(attempted > 0, "No generation attempts were successful"); // Verify by reloading and materializing. - KeyringStore store = KeyringStore.load(ring); + KeyringStore store = KeyringStore.load(session, ring); List aliases = store.aliases(); System.out.println("Reloaded aliases (" + aliases.size() + "): " + aliases); @@ -189,45 +189,15 @@ public class KeyStoreManagementTest { // ---- helpers ---- private static boolean hasAsymmetricDefault(CryptoAlgorithm alg) { - try { - List infos = alg.asymmetricBuildersInfo(); - for (int i = 0; i < infos.size(); i++) { - CryptoAlgorithm.AsymBuilderInfo bi = infos.get(i); - if (bi.defaultKeySpec == null) { - continue; - } - @SuppressWarnings("unchecked") - Class st = (Class) bi.specType; - AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(st); - if (b != null) { - return true; - } - } - } catch (Throwable t) { - return false; - } - return false; + return alg.keyOperations().stream() + .anyMatch(info -> info.operation() == KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE + && info.defaultSpec() != null); } private static boolean hasSymmetricDefault(CryptoAlgorithm alg) { - try { - List infos = alg.symmetricBuildersInfo(); - for (int i = 0; i < infos.size(); i++) { - CryptoAlgorithm.SymBuilderInfo bi = infos.get(i); - if (bi.defaultKeySpec() == null) { - continue; - } - @SuppressWarnings("unchecked") - Class st = (Class) bi.specType(); - SymmetricKeyBuilder b = alg.symmetricKeyBuilder(st); - if (b != null) { - return true; - } - } - } catch (Throwable t) { - return false; - } - return false; + return alg.keyOperations().stream() + .anyMatch(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE + && info.defaultSpec() != null); } private static String sanitize(String id) { diff --git a/app/src/test/java/zeroecho/TagTest.java b/app/src/test/java/zeroecho/TagTest.java index f752286..6e93edb 100644 --- a/app/src/test/java/zeroecho/TagTest.java +++ b/app/src/test/java/zeroecho/TagTest.java @@ -127,7 +127,7 @@ public class TagTest { Path ring = tmp.resolve("ring-ed25519.txt"); KeyAliases ed = generateIntoKeyStore(ring, "Ed25519", "ed"); // sanity - KeyringStore ks = KeyringStore.load(ring); + KeyringStore ks = KeyringStore.load(new zeroecho.sdk.ZeroEchoSession(), ring); assertTrue(ks.contains(ed.pub) && ks.contains(ed.prv), "missing expected aliases"); byte[] pt = randomBytes(4096); diff --git a/ext/src/test/java/zeroecho/ext/integrations/covert/jpeg/JpegExifIntegrationTest.java b/ext/src/test/java/zeroecho/ext/integrations/covert/jpeg/JpegExifIntegrationTest.java index 5e14199..4c002fe 100644 --- a/ext/src/test/java/zeroecho/ext/integrations/covert/jpeg/JpegExifIntegrationTest.java +++ b/ext/src/test/java/zeroecho/ext/integrations/covert/jpeg/JpegExifIntegrationTest.java @@ -55,13 +55,13 @@ import org.junit.jupiter.api.io.TempDir; import conflux.Ctx; import conflux.CtxInterface; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.alg.aes.AesKeyGenSpec; import zeroecho.core.alg.aes.AesSpec; import zeroecho.sdk.builders.alg.AesDataContentBuilder; import zeroecho.sdk.builders.core.DataContentChainBuilder; import zeroecho.sdk.builders.core.PlainBytesBuilder; import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.ZeroEchoSession; class JpegExifIntegrationTest { @@ -91,17 +91,17 @@ class JpegExifIntegrationTest { // AES encryption setup /* - * CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); SecretKey key = - * aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec. - * aes256()); AesSpec spec = + * SecretKey key = zeroEchoSession.keyBuilders().symmetric() + * .generate("AES", AesKeyGenSpec.aes256()); AesSpec spec = * AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build(); - * EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, + * EncryptionContext enc = zeroEchoSession.createContext("AES", KeyUsage.ENCRYPT, key, * spec); CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + * System.nanoTime()); session.put(ConfluxKeys.aad("AES"), aad); ((ContextAware) * enc).setContext(session); */ - SecretKey key = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class) - .generateSecret(AesKeyGenSpec.aes256()); + ZeroEchoSession zeroEchoSession = new ZeroEchoSession(); + SecretKey key = zeroEchoSession.keyBuilders().symmetric() + .generate("AES", AesKeyGenSpec.aes256()); CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime()); byte[] encryptedBytes; @@ -109,7 +109,7 @@ class JpegExifIntegrationTest { // input .add(PlainBytesBuilder.builder().bytes(inputBytes)) // encryption - .add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()) + .add(AesDataContentBuilder.builder(zeroEchoSession).importKeyRaw(key.getEncoded()) // using general AES/GCM/128 without specified header .spec(AesSpec.gcm128(null)) // but let the builder add the default header for storing AAD and IV @@ -152,7 +152,7 @@ class JpegExifIntegrationTest { // input .add(PlainBytesBuilder.builder().bytes(extractedEncryptedBytes)) // encryption - .add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()).spec(AesSpec.gcm128(null)) + .add(AesDataContentBuilder.builder(zeroEchoSession).importKeyRaw(key.getEncoded()).spec(AesSpec.gcm128(null)) // let us use the default header for AAD and IV .withHeader().withAad(aad).context(session)) // and create the pipeline @@ -164,7 +164,7 @@ class JpegExifIntegrationTest { /* * AesSpec spec = * AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build(); - * EncryptionContext dec1 = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, + * EncryptionContext dec1 = zeroEchoSession.createContext("AES", KeyUsage.DECRYPT, * key, spec); ((ContextAware) dec1).setContext(session); // same IV/AAD in ctx * byte[] pt1 = readAll(dec1.attach(new * ByteArrayInputStream(extractedEncryptedBytes))); dec1.close(); diff --git a/lib/src/main/java/zeroecho/core/Capability.java b/lib/src/main/java/zeroecho/core/Capability.java index 2a899dc..3303f9e 100644 --- a/lib/src/main/java/zeroecho/core/Capability.java +++ b/lib/src/main/java/zeroecho/core/Capability.java @@ -1,105 +1,55 @@ /******************************************************************************* * Copyright (C) 2026, Leo Galambos * All rights reserved. - * + * * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software must - * display the following acknowledgement: - * This product includes software developed by the Egothor project. - * - * 4. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * are permitted provided that the conditions in the project LICENSE are met. ******************************************************************************/ package zeroecho.core; import java.security.Key; import java.util.Objects; -import java.util.function.Supplier; -import zeroecho.core.alg.AbstractCryptoAlgorithm; import zeroecho.core.context.CryptoContext; import zeroecho.core.spec.ContextSpec; -import zeroecho.core.spi.ContextConstructorKS; /** - * Immutable descriptor of an algorithm capability. + * Immutable value descriptor of one algorithm context capability. * - *

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

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

Purpose

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

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

- * - *

Thread-safety

{@code Capability} instances are immutable and safe to - * share across threads. + *

The default specification is resolved once during provider construction. + * All components therefore have stable value semantics and are safe for + * concurrent reads.

* + * @param algorithmId canonical algorithm identifier + * @param family algorithm family + * @param role supported key usage + * @param contextType produced context type + * @param keyType accepted key type + * @param specType accepted specification type + * @param defaultSpec non-null resolved default specification * @since 1.0 */ public record Capability(String algorithmId, AlgorithmFamily family, KeyUsage role, - Class contextType, Class keyType, Class specType, - Supplier defaultSpec) { + Class contextType, Class keyType, + Class specType, ContextSpec defaultSpec) { /** - * Creates a new capability descriptor. + * Validates the capability metadata. * - * @param algorithmId identifier of the algorithm this capability belongs to - * @param family high-level algorithm family classification - * @param role supported {@link KeyUsage} role - * @param contextType expected {@link CryptoContext} type for this role - * @param keyType accepted {@link Key} type for this role - * @param specType accepted {@link ContextSpec} type for this role - * @param defaultSpec supplier of a default spec (used when {@code null} is - * passed) - * @throws NullPointerException if any argument is {@code null} + * @throws NullPointerException if a component is {@code null} + * @throws IllegalArgumentException if {@code defaultSpec} is incompatible + * with {@code specType} */ - public Capability(String algorithmId, AlgorithmFamily family, KeyUsage role, - Class contextType, Class keyType, - Class specType, Supplier defaultSpec) { - this.algorithmId = Objects.requireNonNull(algorithmId, "algorithmId must not be null"); - this.family = Objects.requireNonNull(family, "family must not be null"); - this.role = Objects.requireNonNull(role, "role must not be null"); - this.contextType = Objects.requireNonNull(contextType, "contextType must not be null"); - this.keyType = Objects.requireNonNull(keyType, "keyType must not be null"); - this.specType = Objects.requireNonNull(specType, "specType must not be null"); - this.defaultSpec = Objects.requireNonNull(defaultSpec, "defaultSpec must not be null"); + public Capability { + Objects.requireNonNull(algorithmId, "algorithmId must not be null"); + Objects.requireNonNull(family, "family must not be null"); + Objects.requireNonNull(role, "role must not be null"); + Objects.requireNonNull(contextType, "contextType must not be null"); + Objects.requireNonNull(keyType, "keyType must not be null"); + Objects.requireNonNull(specType, "specType must not be null"); + Objects.requireNonNull(defaultSpec, "defaultSpec must not be null"); + if (!specType.isInstance(defaultSpec)) { + throw new IllegalArgumentException("defaultSpec must be an instance of " + specType.getName()); + } } } diff --git a/lib/src/main/java/zeroecho/core/CryptoAlgorithm.java b/lib/src/main/java/zeroecho/core/CryptoAlgorithm.java index 07cae54..39f2337 100644 --- a/lib/src/main/java/zeroecho/core/CryptoAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/CryptoAlgorithm.java @@ -33,32 +33,29 @@ ******************************************************************************/ package zeroecho.core; -import java.io.IOException; -import java.security.GeneralSecurityException; import java.security.Key; -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.PublicKey; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.EnumMap; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Supplier; -import javax.crypto.SecretKey; - import zeroecho.core.context.CryptoContext; import zeroecho.core.err.UnsupportedRoleException; import zeroecho.core.err.UnsupportedSpecException; import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.ContextSpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; -import zeroecho.core.spi.ContextConstructorKS; -import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.ContextFactoryKS; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; +import zeroecho.core.spi.SymmetricKeyGenerator; +import zeroecho.core.spi.SymmetricKeyImporter; /** * Abstract base class for all cryptographic algorithm definitions in ZeroEcho. @@ -70,8 +67,7 @@ import zeroecho.core.spi.SymmetricKeyBuilder; * signatures. *
  • Roles: supported {@link KeyUsage} operations (e.g., ENCRYPT, SIGN) bound * to concrete {@link CryptoContext} constructors.
  • - *
  • Key builders: factories for symmetric and asymmetric key material via - * {@link SymmetricKeyBuilder} and {@link AsymmetricKeyBuilder}.
  • + *
  • Key operations: exact generation and import capabilities.
  • * * *

    Metadata

    Each algorithm instance is uniquely identified by @@ -86,7 +82,7 @@ import zeroecho.core.spi.SymmetricKeyBuilder; *

    Roles and contexts

    Each algorithm may support multiple * {@link KeyUsage} roles. For each role, the algorithm binds a key type, * context type, and optional {@link ContextSpec}. When - * {@link #create(KeyUsage, Key, ContextSpec)} is called: + * {@link #createContext(KeyUsage, Key, ContextSpec)} is called: *
      *
    1. The binding for the role is located.
    2. *
    3. The supplied key and spec are validated against the expected types.
    4. @@ -94,15 +90,9 @@ import zeroecho.core.spi.SymmetricKeyBuilder; * factory. *
    * - *

    Key builders

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

    Key operations

    Providers register generation and import operations + * independently. Lookup returns an interface that guarantees the requested + * operation. * *

    Provider model

    Each algorithm belongs to a {@code providerName}, * allowing multiple providers (e.g., JCA, BouncyCastle, ZeroEcho-native) to @@ -115,7 +105,8 @@ import zeroecho.core.spi.SymmetricKeyBuilder; * *

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

    * @@ -123,6 +114,8 @@ import zeroecho.core.spi.SymmetricKeyBuilder; */ public abstract class CryptoAlgorithm { // NOPMD + private static final String SPEC_TYPE_NULL = "specType must not be null"; + private final String _id; private final String _displayName; private final int _priority; @@ -130,8 +123,18 @@ public abstract class CryptoAlgorithm { // NOPMD private final List capabilities = new ArrayList<>(); private final Map>> ctxBindings = new EnumMap<>(KeyUsage.class); - private final Map, AsymEntry> asymBuilders = new HashMap<>(); - private final Map, SymEntry> symBuilders = new HashMap<>(); + private final Map, AsymmetricKeyPairGenerator> keyPairGenerators = + new LinkedHashMap<>(); + private final Map, PublicKeyImporter> publicKeyImporters = + new LinkedHashMap<>(); + private final Map, PrivateKeyImporter> privateKeyImporters = + new LinkedHashMap<>(); + private final Map, SymmetricKeyGenerator> symmetricKeyGenerators = + new LinkedHashMap<>(); + private final Map, SymmetricKeyImporter> symmetricKeyImporters = + new LinkedHashMap<>(); + private final Map, AlgorithmKeySpec> asymmetricDefaults = new LinkedHashMap<>(); + private final Map, AlgorithmKeySpec> symmetricDefaults = new LinkedHashMap<>(); /** * Create a new algorithm with default priority and provider. @@ -270,15 +273,15 @@ public abstract class CryptoAlgorithm { // NOPMD private final Class ctxType; private final Class keyType; private final Class specType; - private final ContextConstructorKS ctor; + private final ContextFactoryKS factory; private final Supplier defaultSpec; - private RoleBinding(Class ctxType, Class keyType, Class specType, ContextConstructorKS ctor, + private RoleBinding(Class ctxType, Class keyType, Class specType, ContextFactoryKS factory, Supplier defaultSpec) { this.ctxType = ctxType; this.keyType = keyType; this.specType = specType; - this.ctor = ctor; + this.factory = factory; this.defaultSpec = defaultSpec; } @@ -293,7 +296,7 @@ public abstract class CryptoAlgorithm { // NOPMD *

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

    * @@ -309,9 +312,15 @@ public abstract class CryptoAlgorithm { // NOPMD * @param spec type * @throws NullPointerException if any class or factory argument is {@code null} */ - protected final void bind(KeyUsage role, - Class ctxType, Class keyType, Class specType, ContextConstructorKS factory, + protected final void bindContext(KeyUsage role, + Class ctxType, Class keyType, Class specType, ContextFactoryKS factory, Supplier defaultSpec) { + Objects.requireNonNull(role, "role must not be null"); + Objects.requireNonNull(ctxType, "ctxType must not be null"); + Objects.requireNonNull(keyType, "keyType must not be null"); + Objects.requireNonNull(specType, SPEC_TYPE_NULL); + Objects.requireNonNull(factory, "factory must not be null"); + Objects.requireNonNull(defaultSpec, "defaultSpec must not be null"); ctxBindings.computeIfAbsent(role, r -> new ArrayList<>()) .add(new RoleBinding<>(ctxType, keyType, specType, factory, defaultSpec)); } @@ -367,13 +376,18 @@ public abstract class CryptoAlgorithm { // NOPMD * @throws UnsupportedSpecException if no binding accepts the provided key/spec * @throws IllegalStateException if the factory returns an unexpected context * type - * @throws IOException if the factory encounters I/O while - * constructing the context */ @SuppressWarnings("unchecked") - public final C create(KeyUsage role, K key, S spec) - throws IOException { + public final C createContext(KeyUsage role, K key, + S spec) { + return createContextInternal(role, key, spec); + } + @SuppressWarnings("unchecked") + private C createContextInternal(KeyUsage role, + K key, S spec) { + Objects.requireNonNull(role, "role must not be null"); + Objects.requireNonNull(key, "key must not be null"); List> list = ctxBindings.get(role); if (list == null || list.isEmpty()) { throw new UnsupportedRoleException(_id + " does not support role " + role); @@ -381,8 +395,10 @@ public abstract class CryptoAlgorithm { // NOPMD for (RoleBinding rb0 : list) { RoleBinding rb = (RoleBinding) rb0; if (rb.accepts(key, spec)) { - S resolved = (spec != null) ? spec : rb.defaultSpec.get(); - C ctx = rb.ctor.create(key, resolved); + S resolved = (spec != null) ? spec + : Objects.requireNonNull(rb.defaultSpec.get(), "defaultSpec value must not be null"); + C ctx = Objects.requireNonNull(rb.factory.createContext(key, resolved), + _id + " factory returned null"); // Enforce the declared context type contract: if (!rb.ctxType.isInstance(ctx)) { throw new IllegalStateException(_id + " factory returned " + ctx.getClass().getName() @@ -395,451 +411,239 @@ public abstract class CryptoAlgorithm { // NOPMD + (spec == null ? " (default spec)" : " and spec=" + spec.getClass().getName())); } - /** - * Immutable descriptor for an asymmetric builder registered with this - * algorithm. - *

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

    - */ - public static final class AsymBuilderInfo { - public final Class specType; - public final Object defaultKeySpec; - - private AsymBuilderInfo(Class specType, Object defaultKeySpec) { - this.specType = specType; - this.defaultKeySpec = defaultKeySpec; + private S resolveDefault(Class specType, + Supplier defaultSpecOrNull) { + if (defaultSpecOrNull == null) { + return null; } + S value = Objects.requireNonNull(defaultSpecOrNull.get(), "defaultSpec value must not be null"); + if (!specType.isInstance(value)) { + throw new IllegalArgumentException("defaultSpec must be an instance of " + specType.getName()); + } + return value; } /** - * Internal entry binding a registered asymmetric key builder to its default key - * specification supplier. + * Registers asymmetric key-pair generation for one exact specification class. * - *

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

    + *

    The optional default is resolved and validated during registration. + * Registered generators must be safe for concurrent invocation after the + * algorithm is published.

    * - *

    Usage

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

    Thread-safety

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

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

    - * - * @param specType the spec class accepted by {@code builder} - * @param builder builder that can generate/import keys for - * {@code specType} - * @param defaultKeySpecOrNull optional supplier for a default spec (may be - * {@code null}) - * @param spec type - * @throws NullPointerException if {@code specType} or {@code builder} is + * @param specType exact specification class + * @param generator non-null generator + * @param defaultSpecOrNull optional default supplier, evaluated once + * @param specification type + * @throws NullPointerException if a required argument or supplied default is * {@code null} + * @throws IllegalArgumentException if the supplied default has the wrong type */ - protected final void registerAsymmetricKeyBuilder(Class specType, - AsymmetricKeyBuilder builder, Supplier defaultKeySpecOrNull) { - Objects.requireNonNull(specType, "specType must not be null"); - asymBuilders.put(specType, new AsymEntry<>(builder, defaultKeySpecOrNull)); + protected final void registerAsymmetricKeyPairGenerator(Class specType, + AsymmetricKeyPairGenerator generator, Supplier defaultSpecOrNull) { + Objects.requireNonNull(specType, SPEC_TYPE_NULL); + keyPairGenerators.put(specType, Objects.requireNonNull(generator, "generator must not be null")); + asymmetricDefaults.put(specType, resolveDefault(specType, defaultSpecOrNull)); } /** - * Returns the asymmetric key builder associated with the given spec type. + * Registers public-key import for one exact specification class. * - * @param specType spec class used as a lookup key - * @param spec type - * @return the registered {@link AsymmetricKeyBuilder} - * @throws IllegalArgumentException if no builder is registered for - * {@code specType} + * @param specType exact specification class + * @param importer non-null importer safe for concurrent invocation + * @param specification type + * @throws NullPointerException if an argument is {@code null} */ - @SuppressWarnings("unchecked") - public final AsymmetricKeyBuilder asymmetricKeyBuilder(Class specType) { - AsymEntry e = asymBuilders.get(specType); - if (e == null) { - throw new IllegalArgumentException(_id + " has no asymmetric key builder for " + specType.getName()); - } - return (AsymmetricKeyBuilder) e.builder; + protected final void registerPublicKeyImporter(Class specType, + PublicKeyImporter importer) { + Objects.requireNonNull(specType, SPEC_TYPE_NULL); + publicKeyImporters.put(specType, Objects.requireNonNull(importer, "importer must not be null")); } /** - * Returns metadata about all registered asymmetric builders. + * Registers private-key import for one exact specification class. * - *

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

    - * - * @return immutable list of {@link AsymBuilderInfo} descriptors + * @param specType exact specification class + * @param importer non-null importer safe for concurrent invocation + * @param specification type + * @throws NullPointerException if an argument is {@code null} */ - public final List asymmetricBuildersInfo() { - List out = new ArrayList<>(); - for (Map.Entry, AsymEntry> e : asymBuilders.entrySet()) { - Object def = null; - if (e.getValue().defaultKeySpec != null) { - try { - def = e.getValue().defaultKeySpec.get(); - } catch (Throwable t) { // NOPMD - def = null; - } - } - out.add(new AsymBuilderInfo(e.getKey(), def)); - } - return Collections.unmodifiableList(out); + protected final void registerPrivateKeyImporter(Class specType, + PrivateKeyImporter importer) { + Objects.requireNonNull(specType, SPEC_TYPE_NULL); + privateKeyImporters.put(specType, Objects.requireNonNull(importer, "importer must not be null")); } /** - * Immutable descriptor for a symmetric key builder registered with this - * algorithm. + * Registers symmetric-key generation for one exact specification class. * - *

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

    + *

    The optional default is resolved and validated during registration.

    * - *

    Usage

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

    Thread-safety

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

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

    - * - *

    Usage

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

    Thread-safety

    Immutable and thread-safe by design as a - * {@code record}. - * - * @param builder the builder instance that can create or import keys; - * must not be {@code null} - * @param defaultKeySpec supplier for a default specification, or {@code null} - * if no sensible default exists - * @param the type of {@link AlgorithmKeySpec} handled by this - * entry - */ - private record SymEntry(SymmetricKeyBuilder builder, - Supplier defaultKeySpec) { - - /** - * Compact constructor that enforces non-null builder. - * - * @throws NullPointerException if {@code builder} is {@code null} - */ - SymEntry { - Objects.requireNonNull(builder, "builder must not be null"); - } - } - - /** - * Registers a symmetric key builder for a specific spec type. - * - * @param specType the spec class accepted by {@code builder} - * @param builder builder that can generate/import keys for - * {@code specType} - * @param defaultKeySpecOrNull optional supplier for a default spec (may be - * {@code null}) - * @param spec type - * @throws NullPointerException if {@code specType} or {@code builder} is + * @param specType exact specification class + * @param generator non-null generator safe for concurrent invocation + * @param defaultSpecOrNull optional default supplier, evaluated once + * @param specification type + * @throws NullPointerException if a required argument or supplied default is * {@code null} + * @throws IllegalArgumentException if the supplied default has the wrong type */ - protected final void registerSymmetricKeyBuilder(Class specType, - SymmetricKeyBuilder builder, Supplier defaultKeySpecOrNull) { - Objects.requireNonNull(specType, "specType must not be null"); - symBuilders.put(specType, new SymEntry<>(builder, defaultKeySpecOrNull)); + protected final void registerSymmetricKeyGenerator(Class specType, + SymmetricKeyGenerator generator, Supplier defaultSpecOrNull) { + Objects.requireNonNull(specType, SPEC_TYPE_NULL); + symmetricKeyGenerators.put(specType, Objects.requireNonNull(generator, "generator must not be null")); + symmetricDefaults.put(specType, resolveDefault(specType, defaultSpecOrNull)); } /** - * Returns the symmetric key builder associated with the given spec type. + * Registers symmetric-key import for one exact specification class. * - * @param specType spec class used as a lookup key - * @param spec type - * @return the registered {@link SymmetricKeyBuilder} - * @throws IllegalArgumentException if no builder is registered for - * {@code specType} + * @param specType exact specification class + * @param importer non-null importer safe for concurrent invocation + * @param specification type + * @throws NullPointerException if an argument is {@code null} + */ + protected final void registerSymmetricKeyImporter(Class specType, + SymmetricKeyImporter importer) { + Objects.requireNonNull(specType, SPEC_TYPE_NULL); + symmetricKeyImporters.put(specType, Objects.requireNonNull(importer, "importer must not be null")); + } + + private IllegalArgumentException missing(String operation, Class specType) { + return new IllegalArgumentException(_id + " has no " + operation + " for exact spec " + specType.getName()); + } + + /** + * Returns the asymmetric key-pair generator registered for an exact + * specification class. + * + *

    The returned implementation may be shared and invoked concurrently.

    + * + * @param specType exact specification class; subclasses are not matched + * @param specification type + * @return registered generator + * @throws NullPointerException if {@code specType} is {@code null} + * @throws IllegalArgumentException if no generator is registered */ @SuppressWarnings("unchecked") - public final SymmetricKeyBuilder symmetricKeyBuilder(Class specType) { - SymEntry e = symBuilders.get(specType); - if (e == null) { - throw new IllegalArgumentException(_id + " has no symmetric key builder for " + specType.getName()); + public final AsymmetricKeyPairGenerator asymmetricKeyPairGenerator( + Class specType) { + Objects.requireNonNull(specType, SPEC_TYPE_NULL); + AsymmetricKeyPairGenerator generator = keyPairGenerators.get(specType); + if (generator == null) { + throw missing("asymmetric key-pair generator", specType); } - return (SymmetricKeyBuilder) e.builder; + return (AsymmetricKeyPairGenerator) generator; } /** - * Returns metadata about all registered symmetric builders. + * Returns the public-key importer registered for an exact specification class. * - *

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

    + *

    The returned implementation may be shared and invoked concurrently.

    * - * @return immutable list of {@link SymBuilderInfo} descriptors - */ - public final List symmetricBuildersInfo() { - List out = new ArrayList<>(); - for (Map.Entry, SymEntry> e : symBuilders.entrySet()) { - Object def = null; - if (e.getValue().defaultKeySpec != null) { - try { - def = e.getValue().defaultKeySpec.get(); - } catch (Throwable t) { // NOPMD - def = null; - } - } - out.add(new SymBuilderInfo(e.getKey(), def)); - } - return Collections.unmodifiableList(out); - } - - /** - * Generates a fresh symmetric {@link SecretKey} using the registered builder - * for {@code spec}. - * - * @param spec algorithm-specific key specification (must match a registered - * symmetric builder) - * @param spec type - * @return newly generated secret key - * @throws NullPointerException if {@code spec} is {@code null} - * @throws IllegalArgumentException if no symmetric builder is registered for - * {@code spec.getClass()} - * @throws GeneralSecurityException if key generation fails or parameters are - * unsupported + * @param specType exact specification class; subclasses are not matched + * @param specification type + * @return registered importer + * @throws NullPointerException if {@code specType} is {@code null} + * @throws IllegalArgumentException if no importer is registered */ @SuppressWarnings("unchecked") - public final SecretKey generateSecret(S spec) throws GeneralSecurityException { - Objects.requireNonNull(spec, "spec must not be null"); - SymmetricKeyBuilder b = symmetricKeyBuilder((Class) spec.getClass()); - return b.generateSecret(spec); + public final PublicKeyImporter publicKeyImporter(Class specType) { + Objects.requireNonNull(specType, SPEC_TYPE_NULL); + PublicKeyImporter importer = publicKeyImporters.get(specType); + if (importer == null) { + throw missing("public-key importer", specType); + } + return (PublicKeyImporter) importer; } /** - * Imports an existing symmetric {@link SecretKey} using the registered builder - * for {@code spec}. + * Returns the private-key importer registered for an exact specification + * class. * - * @param spec algorithm-specific key specification including raw - * material/format - * @param spec type - * @return wrapped secret key validated against the spec - * @throws NullPointerException if {@code spec} is {@code null} - * @throws IllegalArgumentException if no symmetric builder is registered for - * {@code spec.getClass()} - * @throws GeneralSecurityException if the material is invalid or does not match - * the algorithm + *

    The returned implementation may be shared and invoked concurrently.

    + * + * @param specType exact specification class; subclasses are not matched + * @param specification type + * @return registered importer + * @throws NullPointerException if {@code specType} is {@code null} + * @throws IllegalArgumentException if no importer is registered */ @SuppressWarnings("unchecked") - public final SecretKey importSecret(S spec) throws GeneralSecurityException { - Objects.requireNonNull(spec, "spec must not be null"); - SymmetricKeyBuilder b = symmetricKeyBuilder((Class) spec.getClass()); - return b.importSecret(spec); + public final PrivateKeyImporter privateKeyImporter(Class specType) { + Objects.requireNonNull(specType, SPEC_TYPE_NULL); + PrivateKeyImporter importer = privateKeyImporters.get(specType); + if (importer == null) { + throw missing("private-key importer", specType); + } + return (PrivateKeyImporter) importer; } /** - * Attempts to generate a {@link KeyPair} using the given asymmetric builder's - * default key spec. This method is fully generic and avoids raw types by - * capturing the concrete spec type parameter. + * Returns the symmetric-key generator registered for an exact specification + * class. * - * @param specType the spec class label used for diagnostics - * @param entry the typed asymmetric builder entry - * @param concrete {@link AlgorithmKeySpec} type - * @return a freshly generated key pair - * @throws GeneralSecurityException if the supplier or builder fails - */ - private KeyPair tryGenerateWithDefault(Class specType, - AsymEntry entry) throws GeneralSecurityException { - - if (entry.defaultKeySpec == null) { - throw new GeneralSecurityException("no default spec supplier"); - } - - final S spec; - try { - spec = entry.defaultKeySpec.get(); - } catch (Throwable t) { // NOPMD - throw new GeneralSecurityException("defaultSpec supplier failed for " + specType.getSimpleName() + ": " - + t.getClass().getSimpleName() + ": " + t.getMessage(), t); - } - if (spec == null) { - throw new GeneralSecurityException("defaultSpec supplier returned null for " + specType.getSimpleName()); - } - - // No raw types here: S is captured from entry. - return entry.builder.generateKeyPair(spec); - } - - /** - * Generates a fresh {@link KeyPair} using the first asymmetric builder that - * successfully provides a default key specification. + *

    The returned implementation may be shared and invoked concurrently.

    * - *

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

    - * - *

    Example

    {@code
    -     * CryptoAlgorithm algo = CryptoAlgorithms.require("Ed25519");
    -     * KeyPair kp = algo.generateKeyPair();
    -     * }
    - * - * @return a newly generated key pair using a default spec from one of the - * registered asymmetric builders - * @throws IllegalStateException if no builder declares a default spec - * supplier - * @throws GeneralSecurityException if all candidate builders fail to generate a - * key pair; the exception message details - * individual causes - */ - public final KeyPair generateKeyPair() throws GeneralSecurityException { - StringBuilder reasons = new StringBuilder(128); - boolean attempted = false; - - for (Map.Entry, AsymEntry> e : asymBuilders.entrySet()) { - AsymEntry entry = e.getValue(); - if (entry.defaultKeySpec == null) { - continue; - } - attempted = true; - try { - // Wildcard capture lets the compiler infer without casts. - return tryGenerateWithDefault(e.getKey(), entry); - } catch (GeneralSecurityException ex) { - reasons.append(" - ").append(e.getKey().getSimpleName()).append(": ") - .append(ex.getClass().getSimpleName()).append(": ").append(String.valueOf(ex.getMessage())) - .append('\n'); - // keep trying other builders - } - } - - if (!attempted) { - throw new IllegalStateException(_id + " has no default asymmetric key spec"); - } - throw new GeneralSecurityException( - _id + " failed to generate a default key pair. Reasons:\n" + reasons.toString().trim()); - } - - /** - * Generates a fresh {@link KeyPair} using the registered asymmetric builder for - * {@code spec}. - * - * @param spec algorithm-specific key specification (must match a registered - * asymmetric builder) - * @param spec type - * @return newly generated key pair - * @throws NullPointerException if {@code spec} is {@code null} - * @throws IllegalArgumentException if no asymmetric builder is registered for - * {@code spec.getClass()} - * @throws GeneralSecurityException if key generation fails or parameters are - * unsupported + * @param specType exact specification class; subclasses are not matched + * @param specification type + * @return registered generator + * @throws NullPointerException if {@code specType} is {@code null} + * @throws IllegalArgumentException if no generator is registered */ @SuppressWarnings("unchecked") - public final KeyPair generateKeyPair(S spec) throws GeneralSecurityException { - Objects.requireNonNull(spec, "spec must not be null"); - AsymmetricKeyBuilder b = asymmetricKeyBuilder((Class) spec.getClass()); - return b.generateKeyPair(spec); + public final SymmetricKeyGenerator symmetricKeyGenerator(Class specType) { + Objects.requireNonNull(specType, SPEC_TYPE_NULL); + SymmetricKeyGenerator generator = symmetricKeyGenerators.get(specType); + if (generator == null) { + throw missing("symmetric-key generator", specType); + } + return (SymmetricKeyGenerator) generator; } /** - * Imports a {@link PublicKey} using the registered asymmetric builder for - * {@code spec}. + * Returns the symmetric-key importer registered for an exact specification + * class. * - * @param spec algorithm-specific key specification including encoded public - * material/format - * @param spec type - * @return wrapped public key validated against the spec - * @throws NullPointerException if {@code spec} is {@code null} - * @throws IllegalArgumentException if no asymmetric builder is registered for - * {@code spec.getClass()} - * @throws GeneralSecurityException if the material is invalid or does not match - * the algorithm + *

    The returned implementation may be shared and invoked concurrently.

    + * + * @param specType exact specification class; subclasses are not matched + * @param specification type + * @return registered importer + * @throws NullPointerException if {@code specType} is {@code null} + * @throws IllegalArgumentException if no importer is registered */ @SuppressWarnings("unchecked") - public final PublicKey importPublic(S spec) throws GeneralSecurityException { - Objects.requireNonNull(spec, "spec must not be null"); - AsymmetricKeyBuilder b = asymmetricKeyBuilder((Class) spec.getClass()); - return b.importPublic(spec); + public final SymmetricKeyImporter symmetricKeyImporter(Class specType) { + Objects.requireNonNull(specType, SPEC_TYPE_NULL); + SymmetricKeyImporter importer = symmetricKeyImporters.get(specType); + if (importer == null) { + throw missing("symmetric-key importer", specType); + } + return (SymmetricKeyImporter) importer; } /** - * Imports a {@link PrivateKey} using the registered asymmetric builder for - * {@code spec}. + * Returns deterministic metadata for every exact key operation. * - * @param spec algorithm-specific key specification including encoded private - * material/format - * @param spec type - * @return wrapped private key validated against the spec - * @throws NullPointerException if {@code spec} is {@code null} - * @throws IllegalArgumentException if no asymmetric builder is registered for - * {@code spec.getClass()} - * @throws GeneralSecurityException if the material is invalid or does not match - * the algorithm + * @return immutable metadata ordered by operation and specification class */ - @SuppressWarnings("unchecked") - public final PrivateKey importPrivate(S spec) throws GeneralSecurityException { - Objects.requireNonNull(spec, "spec must not be null"); - AsymmetricKeyBuilder b = asymmetricKeyBuilder((Class) spec.getClass()); - return b.importPrivate(spec); + public final List keyOperations() { + List result = new ArrayList<>(); + addOperationInfo(result, KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE, keyPairGenerators, + asymmetricDefaults); + addOperationInfo(result, KeyOperation.ASYMMETRIC_PUBLIC_IMPORT, publicKeyImporters, Map.of()); + addOperationInfo(result, KeyOperation.ASYMMETRIC_PRIVATE_IMPORT, privateKeyImporters, Map.of()); + addOperationInfo(result, KeyOperation.SYMMETRIC_GENERATE, symmetricKeyGenerators, symmetricDefaults); + addOperationInfo(result, KeyOperation.SYMMETRIC_IMPORT, symmetricKeyImporters, Map.of()); + result.sort(Comparator.comparing(KeyOperationInfo::operation) + .thenComparing(info -> info.specType().getName())); + return List.copyOf(result); + } + + private static void addOperationInfo(List result, KeyOperation operation, + Map, ?> operations, + Map, AlgorithmKeySpec> defaults) { + for (Class specType : operations.keySet()) { + result.add(new KeyOperationInfo(operation, specType, defaults.get(specType))); + } } } diff --git a/lib/src/main/java/zeroecho/core/CryptoAlgorithms.java b/lib/src/main/java/zeroecho/core/CryptoAlgorithms.java index 1773bab..61713be 100644 --- a/lib/src/main/java/zeroecho/core/CryptoAlgorithms.java +++ b/lib/src/main/java/zeroecho/core/CryptoAlgorithms.java @@ -1,615 +1,72 @@ /******************************************************************************* * Copyright (C) 2026, Leo Galambos * All rights reserved. - * + * * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software must - * display the following acknowledgement: - * This product includes software developed by the Egothor project. - * - * 4. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * are permitted provided that the conditions in the project LICENSE are met. ******************************************************************************/ package zeroecho.core; -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.Key; -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.PublicKey; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.ServiceLoader; import java.util.Set; - -import javax.crypto.SecretKey; - -import zeroecho.core.audit.AuditListener; -import zeroecho.core.audit.AuditedContexts; -import zeroecho.core.context.AgreementContext; -import zeroecho.core.context.CryptoContext; -import zeroecho.core.context.DigestContext; -import zeroecho.core.context.EncryptionContext; -import zeroecho.core.context.KemContext; -import zeroecho.core.context.MacContext; -import zeroecho.core.context.SignatureContext; -import zeroecho.core.err.UnsupportedRoleException; -import zeroecho.core.err.UnsupportedSpecException; -import zeroecho.core.policy.CryptoPolicy; -import zeroecho.core.spec.AlgorithmKeySpec; -import zeroecho.core.spec.ContextSpec; +import java.util.TreeMap; /** - * Static façade and registry for {@link CryptoAlgorithm} providers. + * Immutable registry of {@link CryptoAlgorithm} providers. * - *

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

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

    Discovery & identity

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

    Policy

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

    Auditing

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

    Thread-safety

    The registry map and global hooks are safe to read - * concurrently. Hooks are backed by {@code volatile} fields and can be swapped - * at runtime; there is no global lock. + *

    Providers are discovered once through {@link ServiceLoader}, sorted by + * canonical algorithm identifier, and retained in one immutable registry. + * Runtime policy and auditing belong exclusively to explicitly created + * {@link zeroecho.sdk.ZeroEchoSession} instances.

    * * @since 1.0 */ public final class CryptoAlgorithms { - - private static final Map BY_ID; - private static volatile CryptoPolicy POLICY = CryptoPolicy.permissive(); // NOPMD - private static volatile AuditListener AUDIT = AuditListener.noop(); // NOPMD - private static volatile AuditMode AUDIT_MODE = AuditMode.OFF; // NOPMD + private static final Map BY_ID = loadRegistry(); private CryptoAlgorithms() { } - static { - Map m = new HashMap<>(); - for (CryptoAlgorithm a : ServiceLoader.load(CryptoAlgorithm.class)) { - CryptoAlgorithm prev = m.put(a.id(), a); - if (prev != null) { - throw new IllegalStateException("Duplicate algorithm id: " + a.id()); + private static Map loadRegistry() { + Map algorithms = new TreeMap<>(); + for (CryptoAlgorithm algorithm : ServiceLoader.load(CryptoAlgorithm.class)) { + CryptoAlgorithm previous = algorithms.put(algorithm.id(), algorithm); + if (previous != null) { + throw new IllegalStateException("Duplicate algorithm id: " + algorithm.id()); } } - BY_ID = Collections.unmodifiableMap(m); + return Collections.unmodifiableMap(new LinkedHashMap<>(algorithms)); + } + + /* default */ static Map registry() { + return BY_ID; } /** - * Returns the set of available algorithm identifiers discovered via - * {@link ServiceLoader}. + * Returns registered algorithm identifiers in deterministic order. * - *

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

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

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

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

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

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

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

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

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

    - * - *

    Modes

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

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

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

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

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

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

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

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

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

    - * Flow: - *

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

    - * The returned context remains owned by the caller of the factory method. This - * helper does not acquire an additional resource requiring local cleanup. - *

    - * - * @param context type - * @param context source context - * @param listener audit listener - * @param role key usage role - * @return audited wrapper - */ - @SuppressWarnings("unchecked") - /* default */ static C wrapForAudit(CryptoContext context, AuditListener listener, - KeyUsage role) { - return (C) AuditedContexts.wrap(context, listener, role); - } - - /** - * Creates a {@link CryptoContext} using the algorithm’s default spec for the - * role. - * - *

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

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

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

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

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

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

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

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

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

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

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

    - * - * @param algoId algorithm identifier used for audit metadata - * @param provider provider name used for audit metadata - * @param key key to destroy - * @return {@code true} if the key reported destroyed, {@code false} otherwise - */ - public static boolean destroyKey(String algoId, String provider, Key key) { - boolean destroyed = false; - try { - if (key instanceof javax.security.auth.Destroyable) { - javax.security.auth.Destroyable d = (javax.security.auth.Destroyable) key; - if (!d.isDestroyed()) { - d.destroy(); - destroyed = true; - } - } - } catch (Exception ignored) { - // swallow and report via audit only if destroyed - } - if (destroyed) { - AUDIT.onKeyDestroyed(algoId, provider, key); - } - return destroyed; - } - - /** - * Convenience wrapper for - * {@link CryptoAlgorithm#generateSecret(AlgorithmKeySpec)}. - * - * @param id canonical algorithm identifier - * @param spec algorithm-specific key specification - * @param spec type - * @return newly generated secret key - * @throws GeneralSecurityException if key generation fails - * @throws IllegalArgumentException if {@code id} is unknown - */ - public static SecretKey generateSecret(String id, S spec) - throws GeneralSecurityException { - return require(id).generateSecret(spec); - } - - /** - * Convenience wrapper for - * {@link CryptoAlgorithm#generateKeyPair(AlgorithmKeySpec)}. - * - * @param id canonical algorithm identifier - * @param spec algorithm-specific key specification - * @param spec type - * @return newly generated key pair - * @throws GeneralSecurityException if key generation fails - * @throws IllegalArgumentException if {@code id} is unknown - */ - public static KeyPair generateKeyPair(String id, S spec) - throws GeneralSecurityException { - return require(id).generateKeyPair(spec); - } - - /** - * Convenience wrapper for - * {@link CryptoAlgorithm#importPublic(AlgorithmKeySpec)}. - * - * @param id canonical algorithm identifier - * @param spec algorithm-specific key specification - * @param spec type - * @return imported public key - * @throws GeneralSecurityException if import fails - * @throws IllegalArgumentException if {@code id} is unknown - */ - public static PublicKey importPublic(String id, S spec) - throws GeneralSecurityException { - return require(id).importPublic(spec); - } - - /** - * Convenience wrapper for - * {@link CryptoAlgorithm#importPrivate(AlgorithmKeySpec)}. - * - * @param id canonical algorithm identifier - * @param spec algorithm-specific key specification - * @param spec type - * @return imported private key - * @throws GeneralSecurityException if import fails - * @throws IllegalArgumentException if {@code id} is unknown - */ - public static PrivateKey importPrivate(String id, S spec) - throws GeneralSecurityException { - return require(id).importPrivate(spec); - } - - /** - * Convenience wrapper for - * {@link CryptoAlgorithm#importSecret(AlgorithmKeySpec)}. - * - * @param id canonical algorithm identifier - * @param spec algorithm-specific key specification - * @param spec type - * @return imported secret key - * @throws GeneralSecurityException if import fails - * @throws IllegalArgumentException if {@code id} is unknown - */ - public static SecretKey importSecret(String id, S spec) - throws GeneralSecurityException { - return require(id).importSecret(spec); + return algorithm; } } diff --git a/lib/src/main/java/zeroecho/core/CryptoCatalog.java b/lib/src/main/java/zeroecho/core/CryptoCatalog.java index ae17694..86d5690 100644 --- a/lib/src/main/java/zeroecho/core/CryptoCatalog.java +++ b/lib/src/main/java/zeroecho/core/CryptoCatalog.java @@ -33,10 +33,7 @@ ******************************************************************************/ package zeroecho.core; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; -import java.util.ServiceLoader; import zeroecho.core.annotation.Describable; import zeroecho.core.annotation.DisplayName; @@ -47,8 +44,8 @@ import zeroecho.core.annotation.DisplayName; * *

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

    * *
      @@ -60,8 +57,8 @@ import zeroecho.core.annotation.DisplayName; *
    * *

    Identity and uniqueness

    Algorithm ids are treated as canonical keys. - * If two providers expose the same {@linkplain CryptoAlgorithm#id() id}, the - * catalog build fails with {@link IllegalStateException}. + * Duplicate provider identifiers are rejected when the authoritative registry + * is initialized. * *

    Immutability & thread-safety

    After construction, the internal * map is unmodifiable and safe to share across threads. This class performs no @@ -77,10 +74,9 @@ import zeroecho.core.annotation.DisplayName; * * *

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

    * * @since 1.0 @@ -93,26 +89,25 @@ public final class CryptoCatalog { } /** - * Discovers {@link CryptoAlgorithm} implementations via {@link ServiceLoader} - * and returns an immutable catalog snapshot. + * Returns a catalog view of the authoritative registry initialized by + * {@link CryptoAlgorithms}. * *

    - * During loading, algorithm ids are checked for uniqueness. A duplicate id - * results in an {@link IllegalStateException} to prevent ambiguous resolution. + * Provider discovery, deterministic ordering, and duplicate checking occur + * once in {@code CryptoAlgorithms}. This method neither scans providers nor + * copies their collection. *

    * * @return an immutable {@code CryptoCatalog} with all discovered algorithms - * @throws IllegalStateException if two providers declare the same algorithm id + * @throws ExceptionInInitializerError if authoritative provider initialization + * fails */ public static CryptoCatalog load() { - Map m = new HashMap<>(); - ServiceLoader.load(CryptoAlgorithm.class).forEach(a -> { - CryptoAlgorithm prev = m.put(a.id(), a); - if (prev != null) { - throw new IllegalStateException("Duplicate algorithm id: " + a.id()); - } - }); - return new CryptoCatalog(Collections.unmodifiableMap(m)); + return new CryptoCatalog(CryptoAlgorithms.registry()); + } + + /* default */ Map algorithms() { + return algos; } /** @@ -132,9 +127,8 @@ public final class CryptoCatalog { StringBuilder sb = null; for (CryptoAlgorithm a : algos.values()) { boolean hasCaps = !a.listCapabilities().isEmpty(); - boolean hasAsym = !a.asymmetricBuildersInfo().isEmpty(); - boolean hasSym = !a.symmetricBuildersInfo().isEmpty(); - if (!hasCaps && !hasAsym && !hasSym) { + boolean hasKeyOperations = !a.keyOperations().isEmpty(); + if (!hasCaps && !hasKeyOperations) { if (sb == null) { sb = new StringBuilder(50 /* minimal record size */ * 6 /* suggested avg of error records */); // NOPMD } @@ -225,30 +219,20 @@ public final class CryptoCatalog { .append(jsonField("contextType", cap.contextType().getSimpleName())).append(',') .append(jsonField("keyType", cap.keyType().getSimpleName())).append(',') .append(jsonField("specType", cap.specType().getSimpleName())).append(",\"defaultSpec\":") - .append(cap.defaultSpec() == null ? "null" : jsonString(labelOf(cap.defaultSpec().get()))) + .append(cap.defaultSpec() == null ? "null" : jsonString(labelOf(cap.defaultSpec()))) .append('}'); } - sb.append("],\"asymmetricKeyBuilders\":["); - boolean fa = true; - for (CryptoAlgorithm.AsymBuilderInfo kb : a.asymmetricBuildersInfo()) { - if (!fa) { + sb.append("],\"keyOperations\":["); + boolean firstOperation = true; + for (KeyOperationInfo operation : a.keyOperations()) { + if (!firstOperation) { sb.append(','); } - fa = false; - sb.append('{').append(jsonField("specType", kb.specType.getSimpleName())).append(",\"defaultKeySpec\":") - .append(kb.defaultKeySpec == null ? "null" : jsonString(labelOf(kb.defaultKeySpec))) - .append('}'); - } - sb.append("],\"symmetricKeyBuilders\":["); - boolean fs = true; - for (CryptoAlgorithm.SymBuilderInfo kb : a.symmetricBuildersInfo()) { - if (!fs) { - sb.append(','); - } - fs = false; - sb.append('{').append(jsonField("specType", kb.specType().getSimpleName())) - .append(",\"defaultKeySpec\":") - .append(kb.defaultKeySpec() == null ? "null" : jsonString(labelOf(kb.defaultKeySpec()))) + firstOperation = false; + sb.append('{').append(jsonField("operation", operation.operation().name())).append(',') + .append(jsonField("specType", operation.specType().getSimpleName())) + .append(",\"defaultSpec\":") + .append(operation.defaultSpec() == null ? "null" : jsonString(labelOf(operation.defaultSpec()))) .append('}'); } sb.append("]}"); @@ -285,23 +269,17 @@ public final class CryptoCatalog { .append(esc(cap.contextType().getSimpleName())).append("") .append(esc(cap.keyType().getSimpleName())).append("") .append(esc(cap.specType().getSimpleName())).append("") - .append(esc(labelOf(cap.defaultSpec().get()))).append("
    "); + .append(esc(labelOf(cap.defaultSpec()))).append(""); } - sb.append(""); - for (CryptoAlgorithm.AsymBuilderInfo kb : a.asymmetricBuildersInfo()) { - sb.append("") - .append(kb.defaultKeySpec == null ? "" : esc(labelOf(kb.defaultKeySpec))) - .append(""); + sb.append(""); + for (KeyOperationInfo operation : a.keyOperations()) { + sb.append("") + .append(operation.defaultSpec() == null ? "" : esc(labelOf(operation.defaultSpec()))) + .append(""); } - sb.append(""); - for (CryptoAlgorithm.SymBuilderInfo kb : a.symmetricBuildersInfo()) { - sb.append("") - .append(kb.defaultKeySpec() == null ? "" : esc(labelOf(kb.defaultKeySpec()))) - .append(""); - } - sb.append(""); + sb.append(""); } sb.append(""); return sb.toString(); diff --git a/lib/src/main/java/zeroecho/core/KeyOperation.java b/lib/src/main/java/zeroecho/core/KeyOperation.java new file mode 100644 index 0000000..9969a46 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/KeyOperation.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the conditions in the project LICENSE are met. + ******************************************************************************/ +package zeroecho.core; + +/** + * Identifies one exact key-material operation exposed by an algorithm. + * + * @since 1.0 + */ +public enum KeyOperation { + /** Generates a symmetric key. */ + SYMMETRIC_GENERATE, + /** Imports a symmetric key. */ + SYMMETRIC_IMPORT, + /** Generates an asymmetric key pair. */ + ASYMMETRIC_KEY_PAIR_GENERATE, + /** Imports an asymmetric public key. */ + ASYMMETRIC_PUBLIC_IMPORT, + /** Imports an asymmetric private key. */ + ASYMMETRIC_PRIVATE_IMPORT +} diff --git a/lib/src/main/java/zeroecho/core/KeyOperationInfo.java b/lib/src/main/java/zeroecho/core/KeyOperationInfo.java new file mode 100644 index 0000000..5126673 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/KeyOperationInfo.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the conditions in the project LICENSE are met. + ******************************************************************************/ +package zeroecho.core; + +import java.util.Objects; + +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Immutable metadata for one exact key operation. + * + * @param operation operation guaranteed by the associated lookup + * @param specType exact accepted specification type + * @param defaultSpec resolved generation default, or {@code null} for import + * operations and generators without a default + * @since 1.0 + */ +public record KeyOperationInfo(KeyOperation operation, + Class specType, AlgorithmKeySpec defaultSpec) { + + /** + * Validates the metadata invariant. + * + * @throws NullPointerException if {@code operation} or {@code specType} is + * {@code null} + * @throws IllegalArgumentException if a default is incompatible with + * {@code specType}, or an import operation + * declares a default + */ + public KeyOperationInfo { + Objects.requireNonNull(operation, "operation must not be null"); + Objects.requireNonNull(specType, "specType must not be null"); + if (defaultSpec != null && !specType.isInstance(defaultSpec)) { + throw new IllegalArgumentException("defaultSpec must be an instance of " + specType.getName()); + } + if (defaultSpec != null && (operation == KeyOperation.SYMMETRIC_IMPORT + || operation == KeyOperation.ASYMMETRIC_PUBLIC_IMPORT + || operation == KeyOperation.ASYMMETRIC_PRIVATE_IMPORT)) { + throw new IllegalArgumentException("import operations cannot declare a default specification"); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/AbstractCryptoAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/AbstractCryptoAlgorithm.java index 6cdcfb5..f451fd9 100644 --- a/lib/src/main/java/zeroecho/core/alg/AbstractCryptoAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/AbstractCryptoAlgorithm.java @@ -34,6 +34,7 @@ package zeroecho.core.alg; import java.security.Key; +import java.util.Objects; import java.util.function.Supplier; import zeroecho.core.AlgorithmFamily; @@ -42,7 +43,7 @@ import zeroecho.core.CryptoAlgorithm; import zeroecho.core.KeyUsage; import zeroecho.core.context.CryptoContext; import zeroecho.core.spec.ContextSpec; -import zeroecho.core.spi.ContextConstructorKS; +import zeroecho.core.spi.ContextFactoryKS; /** * Convenience base class for concrete {@link CryptoAlgorithm} implementations. @@ -54,7 +55,7 @@ import zeroecho.core.spi.ContextConstructorKS; * *
      *
    1. Binding roles to runtime factories via - * {@link #capability(AlgorithmFamily, KeyUsage, Class, Class, Class, ContextConstructorKS, Supplier)}, + * {@link #capability(AlgorithmFamily, KeyUsage, Class, Class, Class, ContextFactoryKS, Supplier)}, * which registers a {@link KeyUsage role} together with its expected * {@link CryptoContext} type, accepted {@link Key} type, optional * {@link ContextSpec} type, the constructor factory, and a default spec @@ -134,8 +135,8 @@ public abstract class AbstractCryptoAlgorithm extends CryptoAlgorithm { *

      *
        *
      • Runtime binding: delegates to - * {@link CryptoAlgorithm#bind(KeyUsage, Class, Class, Class, ContextConstructorKS, Supplier)} - * so that {@link CryptoAlgorithm#create(KeyUsage, Key, ContextSpec)} can + * {@link CryptoAlgorithm#bindContext(KeyUsage, Class, Class, Class, ContextFactoryKS, Supplier)} + * so that {@link CryptoAlgorithm#createContext(KeyUsage, Key, ContextSpec)} can * construct the appropriate {@link CryptoContext} when invoked.
      • *
      • Metadata publication: creates a {@link Capability} describing this * role (algorithm id, {@link AlgorithmFamily family}, role, context/key/spec @@ -145,7 +146,8 @@ public abstract class AbstractCryptoAlgorithm extends CryptoAlgorithm { *
      * *

      Validation

      Type checks happen at creation time (via {@code bind}) - * and again when {@link CryptoAlgorithm#create(KeyUsage, Key, ContextSpec)} is + * and again when + * {@link CryptoAlgorithm#createContext(KeyUsage, Key, ContextSpec)} is * called. If a factory returns a context not assignable to {@code ctxType}, an * {@link IllegalStateException} will be thrown. * @@ -157,21 +159,24 @@ public abstract class AbstractCryptoAlgorithm extends CryptoAlgorithm { * @param keyType accepted {@link Key} type for this role * @param specType accepted {@link ContextSpec} type (may be a marker type) * @param factory constructor that builds a context for (key, spec) - * @param defaultSpec default spec supplier used when callers pass {@code null} - * spec + * @param defaultSpec supplier of the default spec used when callers pass + * {@code null}; capability metadata resolves one stable + * value during registration, while runtime creation retains + * the supplier contract * @param context type * @param key type * @param spec type - * @throws NullPointerException if any class/factory/supplier argument is - * {@code null} + * @throws NullPointerException if any class, factory, supplier, or metadata + * default value is {@code null} */ protected void capability(AlgorithmFamily family, - KeyUsage role, Class ctxType, Class keyType, Class specType, ContextConstructorKS factory, + KeyUsage role, Class ctxType, Class keyType, Class specType, ContextFactoryKS factory, Supplier defaultSpec) { + S resolvedDefault = Objects.requireNonNull(defaultSpec.get(), "defaultSpec value must not be null"); // bind runtime factory - bind(role, ctxType, keyType, specType, factory, defaultSpec); + bindContext(role, ctxType, keyType, specType, factory, defaultSpec); // publish metadata - addCapability(new Capability(id(), family, role, ctxType, keyType, specType, defaultSpec)); + addCapability(new Capability(id(), family, role, ctxType, keyType, specType, resolvedDefault)); } } diff --git a/lib/src/main/java/zeroecho/core/alg/aes/AesAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/aes/AesAlgorithm.java index 05cf3d2..c26ffe2 100644 --- a/lib/src/main/java/zeroecho/core/alg/aes/AesAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/aes/AesAlgorithm.java @@ -34,7 +34,7 @@ package zeroecho.core.alg.aes; import java.security.GeneralSecurityException; -import java.security.SecureRandom; +import java.util.Arrays; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; @@ -45,7 +45,9 @@ import zeroecho.core.KeyUsage; import zeroecho.core.alg.AbstractCryptoAlgorithm; import zeroecho.core.context.EncryptionContext; import zeroecho.core.spec.VoidSpec; -import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.core.spi.SymmetricKeyGenerator; +import zeroecho.core.spi.SymmetricKeyImporter; +import zeroecho.sdk.util.RandomSupport; /** * AES algorithm registration and capability wiring. @@ -85,49 +87,45 @@ public final class AesAlgorithm extends AbstractCryptoAlgorithm { // Context capabilities capability(AlgorithmFamily.SYMMETRIC, KeyUsage.ENCRYPT, EncryptionContext.class, SecretKey.class, AesSpec.class, - (SecretKey k, AesSpec s) -> new AesCipherContext(this, k, true, s, new SecureRandom()), + (SecretKey k, AesSpec s) -> new AesCipherContext(this, k, true, s, RandomSupport.getRandom()), () -> AesSpec.gcm128(null)); capability(AlgorithmFamily.SYMMETRIC, KeyUsage.DECRYPT, EncryptionContext.class, SecretKey.class, AesSpec.class, - (SecretKey k, AesSpec s) -> new AesCipherContext(this, k, false, s, new SecureRandom()), + (SecretKey k, AesSpec s) -> new AesCipherContext(this, k, false, s, RandomSupport.getRandom()), () -> AesSpec.gcm128(null)); capability(AlgorithmFamily.SYMMETRIC, KeyUsage.ENCRYPT, EncryptionContext.class, SecretKey.class, VoidSpec.class, (SecretKey k, VoidSpec s) -> new AesCipherContext(this, k, true, AesSpec.gcm128(null), - new SecureRandom()), + RandomSupport.getRandom()), () -> VoidSpec.INSTANCE); capability(AlgorithmFamily.SYMMETRIC, KeyUsage.DECRYPT, EncryptionContext.class, SecretKey.class, VoidSpec.class, (SecretKey k, VoidSpec s) -> new AesCipherContext(this, k, false, AesSpec.gcm128(null), - new SecureRandom()), + RandomSupport.getRandom()), () -> VoidSpec.INSTANCE); // Secret generation builder (AesKeyGenSpec) - registerSymmetricKeyBuilder(AesKeyGenSpec.class, new SymmetricKeyBuilder<>() { + registerSymmetricKeyGenerator(AesKeyGenSpec.class, new SymmetricKeyGenerator<>() { @Override public SecretKey generateSecret(AesKeyGenSpec spec) throws GeneralSecurityException { KeyGenerator kg = KeyGenerator.getInstance("AES"); - kg.init(spec.keySizeBits(), new SecureRandom()); + kg.init(spec.keySizeBits(), RandomSupport.getRandom()); return kg.generateKey(); } - - @Override - public SecretKey importSecret(AesKeyGenSpec spec) { - throw new UnsupportedOperationException("Use AesKeyImportSpec for importing AES keys"); - } }, AesKeyGenSpec::aes256); // Secret import builder (AesKeyImportSpec) - registerSymmetricKeyBuilder(AesKeyImportSpec.class, new SymmetricKeyBuilder<>() { - @Override - public SecretKey generateSecret(AesKeyImportSpec spec) { - throw new UnsupportedOperationException("Use AesKeyGenSpec to generate AES keys"); - } + registerSymmetricKeyImporter(AesKeyImportSpec.class, new SymmetricKeyImporter<>() { @Override public SecretKey importSecret(AesKeyImportSpec spec) { - return new SecretKeySpec(spec.key(), "AES"); + byte[] key = spec.key(); + try { + return new SecretKeySpec(key, "AES"); + } finally { + Arrays.fill(key, (byte) 0); + } } - }, null); + }); } } diff --git a/lib/src/main/java/zeroecho/core/alg/aes/AesCipherContext.java b/lib/src/main/java/zeroecho/core/alg/aes/AesCipherContext.java index bda64c3..f6c48e7 100644 --- a/lib/src/main/java/zeroecho/core/alg/aes/AesCipherContext.java +++ b/lib/src/main/java/zeroecho/core/alg/aes/AesCipherContext.java @@ -55,6 +55,7 @@ import zeroecho.core.err.ProviderFailureException; import zeroecho.core.io.CipherTransformInputStreamBuilder; import zeroecho.core.spi.ContextAware; import zeroecho.core.util.Strings; +import zeroecho.sdk.util.RandomSupport; /** * Streaming AES cipher context for GCM / CBC / CTR. @@ -97,7 +98,8 @@ public final class AesCipherContext implements EncryptionContext, ContextAware { * ({@code false}) * @param spec static AES settings (mode/padding and GCM tag bits); not * null - * @param rnd secure random source; if null, a default is created + * @param rnd secure random source; if null, the library's shared source + * is used * @throws NullPointerException if any required parameter is null * @throws IllegalArgumentException if {@code spec} is inconsistent (e.g., GCM * without NOPADDING) @@ -107,7 +109,7 @@ public final class AesCipherContext implements EncryptionContext, ContextAware { this.key = Objects.requireNonNull(key, "secret key must not be null"); this.encrypt = encrypt; this.spec = Objects.requireNonNull(spec, "spec must not be null"); - this.rnd = (rnd != null ? rnd : new SecureRandom()); + this.rnd = (rnd != null ? rnd : RandomSupport.getRandom()); } /** diff --git a/lib/src/main/java/zeroecho/core/alg/aes/AesKeyImportSpec.java b/lib/src/main/java/zeroecho/core/alg/aes/AesKeyImportSpec.java index f528952..bb3e828 100644 --- a/lib/src/main/java/zeroecho/core/alg/aes/AesKeyImportSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/aes/AesKeyImportSpec.java @@ -38,6 +38,9 @@ import java.util.Arrays; import java.util.Base64; import java.util.HexFormat; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.spec.AlgorithmKeySpec; @@ -48,8 +51,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; *

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

      * *

      @@ -58,13 +60,16 @@ import zeroecho.core.spec.AlgorithmKeySpec; *

      * *

      - * Objects of this type are immutable and thread-safe. + * Objects of this type are thread-safe while active and may be destroyed to wipe + * their owned key bytes. Access and marshalling fail after destruction. *

      * * @since 1.0 */ -public final class AesKeyImportSpec implements AlgorithmKeySpec { +public final class AesKeyImportSpec implements AlgorithmKeySpec, Destroyable { private final byte[] key; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; private AesKeyImportSpec(byte[] key) { Objects.requireNonNull(key, "key must not be null"); @@ -96,7 +101,12 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec { */ public static AesKeyImportSpec fromHex(String hex) { Objects.requireNonNull(hex, "hex must not be null"); - return fromRaw(HexFormat.of().parseHex(hex)); + byte[] decoded = HexFormat.of().parseHex(hex); + try { + return fromRaw(decoded); + } finally { + Arrays.fill(decoded, (byte) 0); + } } /** @@ -109,7 +119,12 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec { */ public static AesKeyImportSpec fromBase64(String b64) { Objects.requireNonNull(b64, "base64 must not be null"); - return fromRaw(Base64.getDecoder().decode(b64)); + byte[] decoded = Base64.getDecoder().decode(b64); + try { + return fromRaw(decoded); + } finally { + Arrays.fill(decoded, (byte) 0); + } } /** @@ -118,7 +133,13 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec { * @return the raw key material */ public byte[] key() { - return Arrays.copyOf(key, key.length); + lifecycleLock.lock(); + try { + ensureActive(); + return Arrays.copyOf(key, key.length); + } finally { + lifecycleLock.unlock(); + } } /** @@ -129,7 +150,7 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec { * @return a sequence containing the key data */ public static PairSeq marshal(AesKeyImportSpec spec) { - String k = Base64.getEncoder().withoutPadding().encodeToString(spec.key); + String k = spec.encodedKey(); return PairSeq.of("type", "AES-KEY", "k.b64", k); } @@ -143,21 +164,81 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec { */ public static AesKeyImportSpec unmarshal(PairSeq p) { byte[] out = null; - PairSeq.Cursor cur = p.cursor(); - while (cur.next()) { - String k = cur.key(); - String v = cur.value(); - switch (k) { - case "k.b64" -> out = Base64.getDecoder().decode(v); - case "k.hex" -> out = HexFormat.of().parseHex(v); - case "k.raw" -> out = v.getBytes(StandardCharsets.ISO_8859_1); - default -> { - /* ignore */ } + try { + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + switch (k) { + case "k.b64" -> { + wipe(out); + out = Base64.getDecoder().decode(v); + } + case "k.hex" -> { + wipe(out); + out = HexFormat.of().parseHex(v); + } + case "k.raw" -> { + wipe(out); + out = v.getBytes(StandardCharsets.ISO_8859_1); + } + default -> { + /* ignore */ } + } } + if (out == null) { + throw new IllegalArgumentException("AES key missing (k.b64 / k.hex / k.raw)"); + } + return new AesKeyImportSpec(out); + } finally { + wipe(out); } - if (out == null) { - throw new IllegalArgumentException("AES key missing (k.b64 / k.hex / k.raw)"); + } + + private static void wipe(byte[] current) { + if (current != null) { + Arrays.fill(current, (byte) 0); + } + } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(key); + } finally { + lifecycleLock.unlock(); + } + } + + /** {@inheritDoc} */ + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(key, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + /** {@inheritDoc} */ + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("AES key import specification has been destroyed"); } - return new AesKeyImportSpec(out); } } diff --git a/lib/src/main/java/zeroecho/core/alg/bike/BikeAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/bike/BikeAlgorithm.java index efadda0..5ef65fc 100644 --- a/lib/src/main/java/zeroecho/core/alg/bike/BikeAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/bike/BikeAlgorithm.java @@ -44,6 +44,7 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; import java.security.spec.X509EncodedKeySpec; import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; @@ -56,7 +57,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; import zeroecho.core.context.KemContext; import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.spec.VoidSpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; /** *

      Integration of BIKE (Bit Flipping Key Encapsulation) algorithm

      @@ -84,14 +87,14 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * BikeAlgorithm bike = new BikeAlgorithm(); * * // Generate a key pair - * KeyPair kp = bike.asymmetricKeyBuilder(BikeKeyGenSpec.class) + * KeyPair kp = bike.asymmetricKeyPairGenerator(BikeKeyGenSpec.class) * .generateKeyPair(BikeKeyGenSpec.bike256()); * * // Encapsulation using recipient's public key - * KemContext kemEnc = bike.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE); + * KemContext kemEnc = bike.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE); * * // Decapsulation using private key - * KemContext kemDec = bike.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE); + * KemContext kemDec = bike.createContext(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE); * } * * @since 1.0 @@ -139,7 +142,7 @@ public final class BikeAlgorithm extends AbstractCryptoAlgorithm { .build(); }, () -> VoidSpec.INSTANCE); - registerAsymmetricKeyBuilder(BikeKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + registerAsymmetricKeyPairGenerator(BikeKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() { @Override public KeyPair generateKeyPair(BikeKeyGenSpec spec) throws GeneralSecurityException { ensureProvider(); @@ -152,23 +155,9 @@ public final class BikeAlgorithm extends AbstractCryptoAlgorithm { kpg.initialize(params, new SecureRandom()); return kpg.generateKeyPair(); } - - @Override - public PublicKey importPublic(BikeKeyGenSpec spec) { - throw new UnsupportedOperationException(); - } - - @Override - public PrivateKey importPrivate(BikeKeyGenSpec spec) { - throw new UnsupportedOperationException(); - } }, BikeKeyGenSpec::bike256); - registerAsymmetricKeyBuilder(BikePublicKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(BikePublicKeySpec spec) { - throw new UnsupportedOperationException(); - } + registerPublicKeyImporter(BikePublicKeySpec.class, new PublicKeyImporter<>() { @Override public PublicKey importPublic(BikePublicKeySpec spec) throws GeneralSecurityException { @@ -176,31 +165,22 @@ public final class BikeAlgorithm extends AbstractCryptoAlgorithm { KeyFactory kf = KeyFactory.getInstance("BIKE", providerName()); return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); } + }); - @Override - public PrivateKey importPrivate(BikePublicKeySpec spec) { - throw new UnsupportedOperationException(); - } - }, null); - - registerAsymmetricKeyBuilder(BikePrivateKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(BikePrivateKeySpec spec) { - throw new UnsupportedOperationException(); - } - - @Override - public PublicKey importPublic(BikePrivateKeySpec spec) { - throw new UnsupportedOperationException(); - } + registerPrivateKeyImporter(BikePrivateKeySpec.class, new PrivateKeyImporter<>() { @Override public PrivateKey importPrivate(BikePrivateKeySpec spec) throws GeneralSecurityException { ensureProvider(); KeyFactory kf = KeyFactory.getInstance("BIKE", providerName()); - return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + byte[] encoded = spec.pkcs8(); + try { + return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } finally { + Arrays.fill(encoded, (byte) 0); + } } - }, null); + }); } /** diff --git a/lib/src/main/java/zeroecho/core/alg/bike/BikeKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/bike/BikeKeyGenSpec.java index 4e36831..84f52b9 100644 --- a/lib/src/main/java/zeroecho/core/alg/bike/BikeKeyGenSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/bike/BikeKeyGenSpec.java @@ -44,7 +44,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * *

      Usage

      {@code
        * // Generate a BIKE-192 key pair
      - * KeyPair kp = bikeAlgorithm.asymmetricKeyBuilder(BikeKeyGenSpec.class)
      + * KeyPair kp = bikeAlgorithm.asymmetricKeyPairGenerator(BikeKeyGenSpec.class)
        *                           .generateKeyPair(BikeKeyGenSpec.bike192());
        * }
      * diff --git a/lib/src/main/java/zeroecho/core/alg/bike/BikePrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/bike/BikePrivateKeySpec.java index d4e349d..e73a13e 100644 --- a/lib/src/main/java/zeroecho/core/alg/bike/BikePrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/bike/BikePrivateKeySpec.java @@ -33,8 +33,12 @@ ******************************************************************************/ package zeroecho.core.alg.bike; +import java.util.Arrays; import java.util.Base64; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq.Cursor; @@ -49,7 +53,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; *

      Usage

      {@code
        * // Import a BIKE private key
        * BikePrivateKeySpec spec = new BikePrivateKeySpec(pkcs8Bytes);
      - * PrivateKey key = bikeAlgorithm.importPrivate(spec);
      + * PrivateKey key = bikeAlgorithm.privateKeyImporter(BikePrivateKeySpec.class).importPrivate(spec);
        *
        * // Marshal for storage or transport
        * PairSeq seq = BikePrivateKeySpec.marshal(spec);
      @@ -60,10 +64,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
        *
        * @since 1.0
        */
      -public final class BikePrivateKeySpec implements AlgorithmKeySpec {
      +public final class BikePrivateKeySpec implements AlgorithmKeySpec, Destroyable {
       
           private static final String PKCS8_B64 = "pkcs8.b64";
           private final byte[] pkcs8;
      +    private final ReentrantLock lifecycleLock = new ReentrantLock();
      +    private boolean destroyed;
       
           /**
            * Constructs a new spec from a PKCS#8 encoded private key.
      @@ -81,7 +87,13 @@ public final class BikePrivateKeySpec implements AlgorithmKeySpec {
            * @return cloned PKCS#8 bytes
            */
           public byte[] pkcs8() {
      -        return pkcs8.clone();
      +        lifecycleLock.lock();
      +        try {
      +            ensureActive();
      +            return pkcs8.clone();
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
           }
       
           /**
      @@ -99,7 +111,7 @@ public final class BikePrivateKeySpec implements AlgorithmKeySpec {
            * @return serialized key representation
            */
           public static PairSeq marshal(BikePrivateKeySpec spec) {
      -        String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
      +        String b64 = spec.encodedKey();
               return PairSeq.of("type", "BikePrivateKeySpec", PKCS8_B64, b64);
           }
       
      @@ -120,7 +132,12 @@ public final class BikePrivateKeySpec implements AlgorithmKeySpec {
               if (b64 == null) {
                   throw new IllegalArgumentException("BikePrivateKeySpec: missing pkcs8.b64");
               }
      -        return new BikePrivateKeySpec(Base64.getDecoder().decode(b64));
      +        byte[] decoded = Base64.getDecoder().decode(b64);
      +        try {
      +            return new BikePrivateKeySpec(decoded);
      +        } finally {
      +            Arrays.fill(decoded, (byte) 0);
      +        }
           }
       
           /**
      @@ -132,4 +149,43 @@ public final class BikePrivateKeySpec implements AlgorithmKeySpec {
           public String toString() {
               return "BikePrivateKeySpec[len=" + pkcs8.length + "]";
           }
      +
      +    private String encodedKey() {
      +        lifecycleLock.lock();
      +        try {
      +            ensureActive();
      +            return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
      +    }
      +
      +    @Override
      +    public void destroy() {
      +        lifecycleLock.lock();
      +        try {
      +            if (!destroyed) {
      +                Arrays.fill(pkcs8, (byte) 0);
      +                destroyed = true;
      +            }
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
      +    }
      +
      +    @Override
      +    public boolean isDestroyed() {
      +        lifecycleLock.lock();
      +        try {
      +            return destroyed;
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
      +    }
      +
      +    private void ensureActive() {
      +        if (destroyed) {
      +            throw new IllegalStateException("BIKE private key specification has been destroyed");
      +        }
      +    }
       }
      diff --git a/lib/src/main/java/zeroecho/core/alg/bike/BikePublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/bike/BikePublicKeySpec.java
      index fb52346..7b49dd2 100644
      --- a/lib/src/main/java/zeroecho/core/alg/bike/BikePublicKeySpec.java
      +++ b/lib/src/main/java/zeroecho/core/alg/bike/BikePublicKeySpec.java
      @@ -49,7 +49,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
        * 

      Usage

      {@code
        * // Import a BIKE public key
        * BikePublicKeySpec spec = new BikePublicKeySpec(x509Bytes);
      - * PublicKey key = bikeAlgorithm.importPublic(spec);
      + * PublicKey key = bikeAlgorithm.publicKeyImporter(BikePublicKeySpec.class).importPublic(spec);
        *
        * // Marshal for transport or storage
        * PairSeq seq = BikePublicKeySpec.marshal(spec);
      diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/AbstractChaChaAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/chacha/AbstractChaChaAlgorithm.java
      index 30bd9e7..c2f72e7 100644
      --- a/lib/src/main/java/zeroecho/core/alg/chacha/AbstractChaChaAlgorithm.java
      +++ b/lib/src/main/java/zeroecho/core/alg/chacha/AbstractChaChaAlgorithm.java
      @@ -35,13 +35,15 @@ package zeroecho.core.alg.chacha;
       
       import java.security.GeneralSecurityException;
       import java.security.SecureRandom;
      +import java.util.Arrays;
       
       import javax.crypto.KeyGenerator;
       import javax.crypto.SecretKey;
       import javax.crypto.spec.SecretKeySpec;
       
       import zeroecho.core.alg.AbstractCryptoAlgorithm;
      -import zeroecho.core.spi.SymmetricKeyBuilder;
      +import zeroecho.core.spi.SymmetricKeyGenerator;
      +import zeroecho.core.spi.SymmetricKeyImporter;
       
       /**
        * 

      Abstract base for ChaCha family algorithms

      @@ -64,19 +66,20 @@ import zeroecho.core.spi.SymmetricKeyBuilder; * {@code "ChaCha20"}.
    2. *
    3. Import wraps the raw key material with * {@link javax.crypto.spec.SecretKeySpec}.
    4. - *
    5. Attempts to generate a key via {@code ChaChaKeyImportSpec} or import via - * {@code ChaChaKeyGenSpec} will throw - * {@link UnsupportedOperationException}.
    6. + *
    7. Generation and import are discovered through independent exact + * capabilities, so an unsupported lookup fails before invocation.
    8. * * *

      Example

      {@code
      - * AbstractChaChaAlgorithm algo = ...;
      + * ZeroEchoSession session = new ZeroEchoSession();
        *
        * // Generate a fresh 256-bit key
      - * SecretKey key = algo.generateSecret(ChaChaKeyGenSpec.chacha256());
      + * SecretKey key = session.keyBuilders().symmetric()
      + *                        .generate("ChaCha20", ChaChaKeyGenSpec.chacha256());
        *
        * // Import an existing key
      - * SecretKey imported = algo.importSecret(new ChaChaKeyImportSpec(rawBytes));
      + * SecretKey imported = session.keyBuilders().symmetric()
      + *                             .importKey("ChaCha20", new ChaChaKeyImportSpec(rawBytes));
        * }
      * * @since 1.0 @@ -93,30 +96,26 @@ abstract class AbstractChaChaAlgorithm extends AbstractCryptoAlgorithm { super(id, title); // register once for both algorithms (same 256-bit key) - registerSymmetricKeyBuilder(ChaChaKeyGenSpec.class, new SymmetricKeyBuilder<>() { + registerSymmetricKeyGenerator(ChaChaKeyGenSpec.class, new SymmetricKeyGenerator<>() { @Override public SecretKey generateSecret(ChaChaKeyGenSpec spec) throws GeneralSecurityException { KeyGenerator kg = KeyGenerator.getInstance("ChaCha20"); kg.init(spec.keySizeBits(), new SecureRandom()); return kg.generateKey(); } - - @Override - public SecretKey importSecret(ChaChaKeyGenSpec spec) { - throw new UnsupportedOperationException("Use ChaChaKeyImportSpec for importing ChaCha keys"); - } }, ChaChaKeyGenSpec::chacha256); - registerSymmetricKeyBuilder(ChaChaKeyImportSpec.class, new SymmetricKeyBuilder<>() { - @Override - public SecretKey generateSecret(ChaChaKeyImportSpec spec) { - throw new UnsupportedOperationException("Use ChaChaKeyGenSpec to generate ChaCha keys"); - } + registerSymmetricKeyImporter(ChaChaKeyImportSpec.class, new SymmetricKeyImporter<>() { @Override public SecretKey importSecret(ChaChaKeyImportSpec spec) { - return new SecretKeySpec(spec.key(), "ChaCha20"); + byte[] key = spec.key(); + try { + return new SecretKeySpec(key, "ChaCha20"); + } finally { + Arrays.fill(key, (byte) 0); + } } - }, null); + }); } } diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Algorithm.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Algorithm.java index b75f2d0..ba07442 100644 --- a/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Algorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Algorithm.java @@ -74,19 +74,19 @@ import zeroecho.core.SymmetricHeaderCodec; * corresponding cipher context. * *

      Example

      {@code
      - * var algo = new ChaCha20Poly1305Algorithm();
      - * SecretKey key = algo.generateSecret(ChaChaKeyGenSpec.chacha256());
      + * ZeroEchoSession session = new ZeroEchoSession();
      + * SecretKey key = session.keyBuilders().symmetric()
      + *                        .generate("CHACHA20-POLY1305", ChaChaKeyGenSpec.chacha256());
        *
        * // Encrypt with explicit spec
      - * var spec = ChaCha20Poly1305Spec.builder().header(null).build();
      - * EncryptionContext enc = algo.newContext(
      - *     zeroecho.core.AlgorithmFamily.SYMMETRIC,
      - *     zeroecho.core.KeyUsage.ENCRYPT, key, spec);
      + * ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(null).build();
      + * EncryptionContext enc = session.createContext(
      + *     "CHACHA20-POLY1305", zeroecho.core.KeyUsage.ENCRYPT, key, spec);
        *
        * // Decrypt using VoidSpec default
      - * EncryptionContext dec = algo.newContext(
      - *     zeroecho.core.AlgorithmFamily.SYMMETRIC,
      - *     zeroecho.core.KeyUsage.DECRYPT, key, zeroecho.core.spec.VoidSpec.INSTANCE);
      + * EncryptionContext dec = session.createContext(
      + *     "CHACHA20-POLY1305", zeroecho.core.KeyUsage.DECRYPT, key,
      + *     zeroecho.core.spec.VoidSpec.INSTANCE);
        * }
      * * @since 1.0 diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaKeyImportSpec.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaKeyImportSpec.java index e06c172..6e1f5a3 100644 --- a/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaKeyImportSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaKeyImportSpec.java @@ -38,6 +38,9 @@ import java.util.Arrays; import java.util.Base64; import java.util.HexFormat; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.spec.AlgorithmKeySpec; @@ -66,7 +69,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; *

      Usage

      {@code
        * // Import from raw key bytes
        * ChaChaKeyImportSpec spec = ChaChaKeyImportSpec.fromRaw(keyBytes);
      - * SecretKey key = cryptoAlgorithm.importSecret(spec);
      + * SecretKey key = cryptoAlgorithm.symmetricKeyImporter(ChaChaKeyImportSpec.class).importSecret(spec);
        *
        * // Serialize to PairSeq
        * PairSeq seq = ChaChaKeyImportSpec.marshal(spec);
      @@ -77,8 +80,10 @@ import zeroecho.core.spec.AlgorithmKeySpec;
        *
        * @since 1.0
        */
      -public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
      +public final class ChaChaKeyImportSpec implements AlgorithmKeySpec, Destroyable {
           private final byte[] key;
      +    private final ReentrantLock lifecycleLock = new ReentrantLock();
      +    private boolean destroyed;
       
           /**
            * Creates a new import spec with the given raw key.
      @@ -112,7 +117,12 @@ public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
            * @return spec wrapping the decoded key
            */
           public static ChaChaKeyImportSpec fromHex(String hex) {
      -        return fromRaw(HexFormat.of().parseHex(hex));
      +        byte[] decoded = HexFormat.of().parseHex(hex);
      +        try {
      +            return fromRaw(decoded);
      +        } finally {
      +            Arrays.fill(decoded, (byte) 0);
      +        }
           }
       
           /**
      @@ -122,7 +132,12 @@ public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
            * @return spec wrapping the decoded key
            */
           public static ChaChaKeyImportSpec fromBase64(String b64) {
      -        return fromRaw(Base64.getDecoder().decode(b64));
      +        byte[] decoded = Base64.getDecoder().decode(b64);
      +        try {
      +            return fromRaw(decoded);
      +        } finally {
      +            Arrays.fill(decoded, (byte) 0);
      +        }
           }
       
           /**
      @@ -131,7 +146,13 @@ public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
            * @return 32-byte key array
            */
           public byte[] key() {
      -        return Arrays.copyOf(key, key.length);
      +        lifecycleLock.lock();
      +        try {
      +            ensureActive();
      +            return Arrays.copyOf(key, key.length);
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
           }
       
           /**
      @@ -141,7 +162,7 @@ public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
            * @return serialized key representation
            */
           public static PairSeq marshal(ChaChaKeyImportSpec spec) {
      -        String k = Base64.getEncoder().withoutPadding().encodeToString(spec.key);
      +        String k = spec.encodedKey();
               return PairSeq.of("type", "CHACHA-KEY", "k.b64", k);
           }
       
      @@ -163,21 +184,81 @@ public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
            */
           public static ChaChaKeyImportSpec unmarshal(PairSeq p) {
               byte[] out = null;
      -        PairSeq.Cursor c = p.cursor();
      -        while (c.next()) {
      -            String k = c.key();
      -            String v = c.value();
      -            switch (k) {
      -                case "k.b64" -> out = Base64.getDecoder().decode(v);
      -                case "k.hex" -> out = HexFormat.of().parseHex(v);
      -                case "k.raw" -> out = v.getBytes(StandardCharsets.ISO_8859_1);
      -                default -> {
      +        try {
      +            PairSeq.Cursor c = p.cursor();
      +            while (c.next()) {
      +                String k = c.key();
      +                String v = c.value();
      +                switch (k) {
      +                    case "k.b64" -> {
      +                        wipe(out);
      +                        out = Base64.getDecoder().decode(v);
      +                    }
      +                    case "k.hex" -> {
      +                        wipe(out);
      +                        out = HexFormat.of().parseHex(v);
      +                    }
      +                    case "k.raw" -> {
      +                        wipe(out);
      +                        out = v.getBytes(StandardCharsets.ISO_8859_1);
      +                    }
      +                    default -> {
      +                    }
                       }
                   }
      +            if (out == null) {
      +                throw new IllegalArgumentException("ChaCha20 key missing (k.b64 / k.hex / k.raw)");
      +            }
      +            return new ChaChaKeyImportSpec(out);
      +        } finally {
      +            wipe(out);
               }
      -        if (out == null) {
      -            throw new IllegalArgumentException("ChaCha20 key missing (k.b64 / k.hex / k.raw)");
      +    }
      +
      +    private static void wipe(byte[] current) {
      +        if (current != null) {
      +            Arrays.fill(current, (byte) 0);
      +        }
      +    }
      +
      +    private String encodedKey() {
      +        lifecycleLock.lock();
      +        try {
      +            ensureActive();
      +            return Base64.getEncoder().withoutPadding().encodeToString(key);
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
      +    }
      +
      +    /** {@inheritDoc} */
      +    @Override
      +    public void destroy() {
      +        lifecycleLock.lock();
      +        try {
      +            if (!destroyed) {
      +                Arrays.fill(key, (byte) 0);
      +                destroyed = true;
      +            }
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
      +    }
      +
      +    /** {@inheritDoc} */
      +    @Override
      +    public boolean isDestroyed() {
      +        lifecycleLock.lock();
      +        try {
      +            return destroyed;
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
      +    }
      +
      +    private void ensureActive() {
      +        if (destroyed) {
      +            throw new IllegalStateException("ChaCha key import specification has been destroyed");
               }
      -        return new ChaChaKeyImportSpec(out);
           }
       }
      diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/package-info.java b/lib/src/main/java/zeroecho/core/alg/chacha/package-info.java
      index 1f2327e..680289b 100644
      --- a/lib/src/main/java/zeroecho/core/alg/chacha/package-info.java
      +++ b/lib/src/main/java/zeroecho/core/alg/chacha/package-info.java
      @@ -37,9 +37,10 @@
        * 

      * This package provides the ChaCha capability set for the core layer, including * the stream cipher ChaCha20 and the AEAD construction ChaCha20-Poly1305. The - * module contains algorithm descriptors, streaming cipher contexts, immutable - * specifications, optional header codecs for runtime parameters, and symmetric - * key import/generation specifications. The design favors safe defaults + * module contains algorithm descriptors, streaming cipher contexts, + * configuration specifications, optional header codecs for runtime parameters, + * and symmetric key import/generation specifications. Key import + * specifications are destroyable. The design favors safe defaults * (12-byte nonces, 128-bit AEAD tag), explicit role-to-context binding, and a * clear separation between static configuration and per-operation parameters. *

      diff --git a/lib/src/main/java/zeroecho/core/alg/cmce/CmceAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/cmce/CmceAlgorithm.java index 84d5475..686bb09 100644 --- a/lib/src/main/java/zeroecho/core/alg/cmce/CmceAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/cmce/CmceAlgorithm.java @@ -44,6 +44,7 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; import java.security.spec.X509EncodedKeySpec; import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; @@ -56,7 +57,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; import zeroecho.core.context.KemContext; import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.spec.VoidSpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; /** *

      Classic McEliece (CMCE) algorithm adapter

      @@ -106,15 +109,15 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * CmceAlgorithm alg = new CmceAlgorithm(); * * // Generate a key pair with a chosen CMCE variant. - * KeyPair kp = alg.asymmetricKeyBuilder(CmceKeyGenSpec.class) + * KeyPair kp = alg.asymmetricKeyPairGenerator(CmceKeyGenSpec.class) * .generateKeyPair(CmceKeyGenSpec.mceliece8192128f()); * * // Create a KEM encapsulation context with the recipient public key. - * KemContext enc = alg.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE); + * KemContext enc = alg.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE); * * // Create an agreement initiator context backed by CMCE KEM. * MessageAgreementContext initiator = - * alg.create(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE); + * alg.createContext(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE); * }
      * * @since 1.0 @@ -169,7 +172,7 @@ public final class CmceAlgorithm extends AbstractCryptoAlgorithm { .build(); }, () -> VoidSpec.INSTANCE); - registerAsymmetricKeyBuilder(CmceKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + registerAsymmetricKeyPairGenerator(CmceKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() { @Override public KeyPair generateKeyPair(CmceKeyGenSpec spec) throws GeneralSecurityException { ensureProvider(); @@ -189,23 +192,9 @@ public final class CmceAlgorithm extends AbstractCryptoAlgorithm { kpg.initialize(params, new SecureRandom()); return kpg.generateKeyPair(); } - - @Override - public PublicKey importPublic(CmceKeyGenSpec spec) { - throw new UnsupportedOperationException(); - } - - @Override - public PrivateKey importPrivate(CmceKeyGenSpec spec) { - throw new UnsupportedOperationException(); - } }, CmceKeyGenSpec::mceliece8192128f); - registerAsymmetricKeyBuilder(CmcePublicKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(CmcePublicKeySpec spec) { - throw new UnsupportedOperationException(); - } + registerPublicKeyImporter(CmcePublicKeySpec.class, new PublicKeyImporter<>() { @Override public PublicKey importPublic(CmcePublicKeySpec spec) throws GeneralSecurityException { @@ -213,31 +202,22 @@ public final class CmceAlgorithm extends AbstractCryptoAlgorithm { KeyFactory kf = KeyFactory.getInstance("CMCE", providerName()); return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); } + }); - @Override - public PrivateKey importPrivate(CmcePublicKeySpec spec) { - throw new UnsupportedOperationException(); - } - }, null); - - registerAsymmetricKeyBuilder(CmcePrivateKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(CmcePrivateKeySpec spec) { - throw new UnsupportedOperationException(); - } - - @Override - public PublicKey importPublic(CmcePrivateKeySpec spec) { - throw new UnsupportedOperationException(); - } + registerPrivateKeyImporter(CmcePrivateKeySpec.class, new PrivateKeyImporter<>() { @Override public PrivateKey importPrivate(CmcePrivateKeySpec spec) throws GeneralSecurityException { ensureProvider(); KeyFactory kf = KeyFactory.getInstance("CMCE", providerName()); - return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + byte[] encoded = spec.pkcs8(); + try { + return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } finally { + Arrays.fill(encoded, (byte) 0); + } } - }, null); + }); } private static void ensureProvider() throws NoSuchProviderException { diff --git a/lib/src/main/java/zeroecho/core/alg/cmce/CmceKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/cmce/CmceKeyGenSpec.java index 6fcd143..a54f936 100644 --- a/lib/src/main/java/zeroecho/core/alg/cmce/CmceKeyGenSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/cmce/CmceKeyGenSpec.java @@ -52,7 +52,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; *
      {@code
        * // Generate a key pair for McEliece 8192128F (256-bit security, fast)
        * CmceKeyGenSpec spec = CmceKeyGenSpec.mceliece8192128f();
      - * KeyPair kp = alg.asymmetricKeyBuilder(CmceKeyGenSpec.class).generateKeyPair(spec);
      + * KeyPair kp = alg.asymmetricKeyPairGenerator(CmceKeyGenSpec.class).generateKeyPair(spec);
        * }
      * * @since 1.0 diff --git a/lib/src/main/java/zeroecho/core/alg/cmce/CmcePrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/cmce/CmcePrivateKeySpec.java index 0c70a52..f824d1e 100644 --- a/lib/src/main/java/zeroecho/core/alg/cmce/CmcePrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/cmce/CmcePrivateKeySpec.java @@ -33,8 +33,12 @@ ******************************************************************************/ package zeroecho.core.alg.cmce; +import java.util.Arrays; import java.util.Base64; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq.Cursor; @@ -49,8 +53,8 @@ import zeroecho.core.spec.AlgorithmKeySpec; *

      * *

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

      * *

      Marshalling

      @@ -76,10 +80,12 @@ import zeroecho.core.spec.AlgorithmKeySpec; * * @since 1.0 */ -public final class CmcePrivateKeySpec implements AlgorithmKeySpec { +public final class CmcePrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] pkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Creates a new specification from a PKCS#8-encoded CMCE private key. @@ -101,7 +107,13 @@ public final class CmcePrivateKeySpec implements AlgorithmKeySpec { * @return a fresh copy of the underlying PKCS#8 encoding */ public byte[] pkcs8() { - return pkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return pkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -118,7 +130,7 @@ public final class CmcePrivateKeySpec implements AlgorithmKeySpec { * @throws NullPointerException if {@code spec} is null */ public static PairSeq marshal(CmcePrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "CmcePrivateKeySpec", PKCS8_B64, b64); } @@ -144,7 +156,12 @@ public final class CmcePrivateKeySpec implements AlgorithmKeySpec { if (b64 == null) { throw new IllegalArgumentException("CmcePrivateKeySpec: missing pkcs8.b64"); } - return new CmcePrivateKeySpec(Base64.getDecoder().decode(b64)); + byte[] decoded = Base64.getDecoder().decode(b64); + try { + return new CmcePrivateKeySpec(decoded); + } finally { + Arrays.fill(decoded, (byte) 0); + } } /** @@ -160,4 +177,43 @@ public final class CmcePrivateKeySpec implements AlgorithmKeySpec { public String toString() { return "CmcePrivateKeySpec[len=" + pkcs8.length + "]"; } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(pkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(pkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("CMCE private key specification has been destroyed"); + } + } } diff --git a/lib/src/main/java/zeroecho/core/alg/cmce/package-info.java b/lib/src/main/java/zeroecho/core/alg/cmce/package-info.java index 15a536d..4712bd0 100644 --- a/lib/src/main/java/zeroecho/core/alg/cmce/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/cmce/package-info.java @@ -67,8 +67,8 @@ * selects a CMCE parameter set (variant) used by the key-pair builder. *
    9. Key import specs: {@link zeroecho.core.alg.cmce.CmcePublicKeySpec} * wraps X.509 public keys and {@link zeroecho.core.alg.cmce.CmcePrivateKeySpec} - * wraps PKCS#8 private keys; both are immutable and defensively copy their byte - * arrays.
    10. + * wraps PKCS#8 private keys; both defensively copy their byte arrays, and the + * private-key form is destroyable. * * *

      Provider requirements

      diff --git a/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaAgreementContext.java b/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaAgreementContext.java index 708d838..333759e 100644 --- a/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaAgreementContext.java +++ b/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaAgreementContext.java @@ -33,21 +33,18 @@ ******************************************************************************/ package zeroecho.core.alg.common.agreement; -import java.security.GeneralSecurityException; import java.security.Key; import java.security.PrivateKey; import java.security.PublicKey; -import javax.crypto.KeyAgreement; - import zeroecho.core.CryptoAlgorithm; import zeroecho.core.context.AgreementContext; /** *

      Generic JCA-based Key Agreement Context

      * - * An {@link AgreementContext} backed by the standard JCA {@link KeyAgreement} - * API. This class supports elliptic-curve and modern Diffie-Hellman variants + * An {@link AgreementContext} backed by the standard JCA key-agreement API. + * This class supports elliptic-curve and modern Diffie-Hellman variants * such as ECDH, XDH (X25519, X448), and others provided by the runtime or * configured provider. * @@ -75,12 +72,9 @@ import zeroecho.core.context.AgreementContext; * * @since 1.0 */ -public class GenericJcaAgreementContext implements AgreementContext { +public final class GenericJcaAgreementContext implements AgreementContext { private final CryptoAlgorithm algorithm; - private final PrivateKey privateKey; - private final String jcaName; // e.g., "ECDH" or "XDH" (or "X25519"/"X448") - private final String provider; // null => default - private PublicKey peer; + private final JcaAgreementEngine engine; /** * Creates a new JCA-based agreement context. @@ -95,10 +89,8 @@ public class GenericJcaAgreementContext implements AgreementContext { * is {@code null} */ public GenericJcaAgreementContext(CryptoAlgorithm alg, PrivateKey priv, String jcaName, String provider) { - this.algorithm = alg; - this.privateKey = priv; - this.jcaName = jcaName; - this.provider = provider; + this.algorithm = java.util.Objects.requireNonNull(alg, "alg must not be null"); + this.engine = new JcaAgreementEngine(priv, jcaName, provider); } /** @@ -118,7 +110,7 @@ public class GenericJcaAgreementContext implements AgreementContext { */ @Override public Key key() { - return privateKey; + return engine.privateKey(); } /** @@ -133,7 +125,7 @@ public class GenericJcaAgreementContext implements AgreementContext { */ @Override public void setPeerPublic(PublicKey peer) { - this.peer = peer; + engine.setPeerPublic(peer); } /** @@ -141,7 +133,7 @@ public class GenericJcaAgreementContext implements AgreementContext { * previously assigned peer public key. * *

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

      * @@ -152,18 +144,7 @@ public class GenericJcaAgreementContext implements AgreementContext { */ @Override public byte[] deriveSecret() { - if (peer == null) { - throw new IllegalStateException("Peer public key not set"); - } - try { - KeyAgreement ka = (provider == null) ? KeyAgreement.getInstance(jcaName) - : KeyAgreement.getInstance(jcaName, provider); - ka.init(privateKey); - ka.doPhase(peer, true); - return ka.generateSecret(); - } catch (GeneralSecurityException e) { - throw new IllegalArgumentException("KeyAgreement failed for " + jcaName, e); - } + return engine.deriveSecret(); } /** diff --git a/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaMessageAgreementContext.java b/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaMessageAgreementContext.java index d9072e3..3cee45a 100644 --- a/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaMessageAgreementContext.java +++ b/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaMessageAgreementContext.java @@ -104,9 +104,9 @@ import zeroecho.core.context.MessageAgreementContext; * * @since 1.0 */ -public final class GenericJcaMessageAgreementContext extends GenericJcaAgreementContext - implements MessageAgreementContext { +public final class GenericJcaMessageAgreementContext implements MessageAgreementContext { + private final GenericJcaAgreementContext agreement; private final PublicKey localPublic; private final String keyFactoryAlg; private final String keyFactoryProvider; @@ -139,7 +139,8 @@ public final class GenericJcaMessageAgreementContext extends GenericJcaAgreement */ public GenericJcaMessageAgreementContext(CryptoAlgorithm alg, KeyPairKey keyPairKey, String jcaAgreementName, String agreementProvider, String keyFactoryAlg, String keyFactoryProvider) { - super(Objects.requireNonNull(alg, "alg"), Objects.requireNonNull(keyPairKey, "keyPairKey").privateKey(), + this.agreement = new GenericJcaAgreementContext(Objects.requireNonNull(alg, "alg"), + Objects.requireNonNull(keyPairKey, "keyPairKey").privateKey(), Objects.requireNonNull(jcaAgreementName, "jcaAgreementName"), agreementProvider); this.localPublic = Objects.requireNonNull(keyPairKey.publicKey(), "keyPairKey.public"); this.keyFactoryAlg = Objects.requireNonNull(keyFactoryAlg, "keyFactoryAlg"); @@ -199,12 +200,18 @@ public final class GenericJcaMessageAgreementContext extends GenericJcaAgreement @Override public void setPeerMessage(byte[] message) { if (message == null) { - setPeerPublic(null); + agreement.setPeerPublic(null); return; } PublicKey peerPublic = importPeerPublic(message); - setPeerPublic(peerPublic); + agreement.setPeerPublic(peerPublic); + } + + /** {@inheritDoc} */ + @Override + public void setPeerPublic(PublicKey peer) { + agreement.setPeerPublic(peer); } /** @@ -232,4 +239,28 @@ public final class GenericJcaMessageAgreementContext extends GenericJcaAgreement throw new IllegalArgumentException("Failed to import peer public key using KeyFactory " + keyFactoryAlg, e); } } + + /** {@inheritDoc} */ + @Override + public byte[] deriveSecret() { + return agreement.deriveSecret(); + } + + /** {@inheritDoc} */ + @Override + public CryptoAlgorithm algorithm() { + return agreement.algorithm(); + } + + /** {@inheritDoc} */ + @Override + public java.security.Key key() { + return agreement.key(); + } + + /** {@inheritDoc} */ + @Override + public void close() { + agreement.close(); + } } diff --git a/lib/src/main/java/zeroecho/core/alg/common/agreement/JcaAgreementEngine.java b/lib/src/main/java/zeroecho/core/alg/common/agreement/JcaAgreementEngine.java new file mode 100644 index 0000000..6271420 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/common/agreement/JcaAgreementEngine.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the conditions in the project LICENSE are met. + ******************************************************************************/ +package zeroecho.core.alg.common.agreement; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Objects; + +import javax.crypto.KeyAgreement; + +/** + * Package-private reusable JCA agreement mechanics. + */ +final class JcaAgreementEngine { + private final PrivateKey privateKey; + private final String jcaName; + private final String provider; + private PublicKey peer; + + /* default */ JcaAgreementEngine(PrivateKey privateKey, String jcaName, String provider) { + this.privateKey = Objects.requireNonNull(privateKey, "privateKey must not be null"); + this.jcaName = Objects.requireNonNull(jcaName, "jcaName must not be null"); + this.provider = provider; + } + + /* default */ PrivateKey privateKey() { + return privateKey; + } + + /* default */ void setPeerPublic(PublicKey peer) { + this.peer = peer; + } + + /* default */ byte[] deriveSecret() { + if (peer == null) { + throw new IllegalStateException("Peer public key not set"); + } + try { + KeyAgreement agreement = provider == null ? KeyAgreement.getInstance(jcaName) + : KeyAgreement.getInstance(jcaName, provider); + agreement.init(privateKey); + agreement.doPhase(peer, true); + return agreement.generateSecret(); + } catch (GeneralSecurityException exception) { + throw new IllegalArgumentException("KeyAgreement failed for " + jcaName, exception); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEdDSAKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEdDSAKeyGenBuilder.java index 4659009..57b5130 100644 --- a/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEdDSAKeyGenBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEdDSAKeyGenBuilder.java @@ -36,11 +36,9 @@ package zeroecho.core.alg.common.eddsa; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyPairGenerator; -import java.security.PrivateKey; -import java.security.PublicKey; import zeroecho.core.spec.AlgorithmKeySpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; /** *

      Abstract EdDSA Key-Pair Builder

      @@ -71,7 +69,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * * @since 1.0 */ -public abstract class AbstractEdDSAKeyGenBuilder implements AsymmetricKeyBuilder { +public abstract class AbstractEdDSAKeyGenBuilder implements AsymmetricKeyPairGenerator { /** * Returns the JCA algorithm name understood by {@link KeyPairGenerator}. * @@ -92,9 +90,8 @@ public abstract class AbstractEdDSAKeyGenBuilder imp * Generates a new EdDSA key pair using JCA defaults. * *

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

      * * @param spec algorithm-specific key specification (currently unused) @@ -107,38 +104,4 @@ public abstract class AbstractEdDSAKeyGenBuilder imp KeyPairGenerator kpg = KeyPairGenerator.getInstance(jcaKeyPairAlg()); return kpg.generateKeyPair(); } - - /** - * Always throws, as this builder does not support public key import. - * - *

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

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

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

      - * - * @param spec algorithm-specific key specification - * @return never returns normally - * @throws UnsupportedOperationException always thrown - */ - @Override - public PrivateKey importPrivate(S spec) { - throw new UnsupportedOperationException("Use the corresponding PrivateKeySpec to import a private key."); - } } diff --git a/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEncodedPrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEncodedPrivateKeyBuilder.java index b2114d4..bb2f06e 100644 --- a/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEncodedPrivateKeyBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEncodedPrivateKeyBuilder.java @@ -35,13 +35,12 @@ package zeroecho.core.alg.common.eddsa; import java.security.GeneralSecurityException; import java.security.KeyFactory; -import java.security.KeyPair; import java.security.PrivateKey; -import java.security.PublicKey; import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; import zeroecho.core.spec.AlgorithmKeySpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.PrivateKeyImporter; /** *

      Abstract EdDSA Encoded Private Key Builder

      @@ -74,7 +73,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * * @since 1.0 */ -public abstract class AbstractEncodedPrivateKeyBuilder implements AsymmetricKeyBuilder { +public abstract class AbstractEncodedPrivateKeyBuilder implements PrivateKeyImporter { /** * Returns the canonical JCA algorithm identifier used by * {@link KeyFactory#getInstance(String)}. @@ -101,32 +100,6 @@ public abstract class AbstractEncodedPrivateKeyBuilderAbstract EdDSA Encoded Public Key Builder @@ -75,7 +73,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * * @since 1.0 */ -public abstract class AbstractEncodedPublicKeyBuilder implements AsymmetricKeyBuilder { +public abstract class AbstractEncodedPublicKeyBuilder implements PublicKeyImporter { /** * Returns the canonical JCA algorithm identifier used by @@ -103,19 +101,6 @@ public abstract class AbstractEncodedPublicKeyBuilder */ -final class Stream extends AbstractPassthroughInputStream { - private static final Logger LOG = Logger.getLogger(Stream.class.getName()); +final class SignatureStream extends AbstractPassthroughInputStream { + private static final Logger LOG = Logger.getLogger(SignatureStream.class.getName()); /** Cached trailer in sign mode: computed once from {@link Signature#sign()}. */ private byte[] signature; @@ -108,7 +108,7 @@ final class Stream extends AbstractPassthroughInputStream { * @param strategy verification predicate used in verify mode; ignored in * sign mode; must not be {@code null} in verify mode */ - /* package */ Stream(final Signature engine, final boolean signMode, final InputStream upstream, + /* package */ SignatureStream(final Signature engine, final boolean signMode, final InputStream upstream, final int bodyBufSize, final byte[] expectedTag, final VerificationBiPredicate strategy) { super(upstream, bodyBufSize); this.engine = engine; diff --git a/lib/src/main/java/zeroecho/core/alg/common/sig/package-info.java b/lib/src/main/java/zeroecho/core/alg/common/sig/package-info.java index 9e875d7..9a618c9 100644 --- a/lib/src/main/java/zeroecho/core/alg/common/sig/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/common/sig/package-info.java @@ -60,7 +60,7 @@ * configured {@link java.security.Signature}, resolves a fixed tag length (via * resolvers), and exposes a one-shot {@code wrap(InputStream)} API. * Verification behavior is controlled by a pluggable comparison approach. - *
    11. Stream - internal passthrough input stream that feeds chunks to + *
    12. SignatureStream - internal passthrough input stream that feeds chunks to * the signature engine, emits the trailer in SIGN mode, and performs final * verification in VERIFY mode.
    13. * diff --git a/lib/src/main/java/zeroecho/core/alg/dh/DhAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/dh/DhAlgorithm.java index f5561a3..2213df2 100644 --- a/lib/src/main/java/zeroecho/core/alg/dh/DhAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/dh/DhAlgorithm.java @@ -35,11 +35,11 @@ package zeroecho.core.alg.dh; import java.security.GeneralSecurityException; import java.security.KeyFactory; -import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; import zeroecho.core.AlgorithmFamily; import zeroecho.core.KeyUsage; @@ -49,7 +49,8 @@ import zeroecho.core.alg.common.agreement.GenericJcaMessageAgreementContext; import zeroecho.core.alg.common.agreement.KeyPairKey; import zeroecho.core.context.AgreementContext; import zeroecho.core.context.MessageAgreementContext; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; /** * Diffie-Hellman algorithm registration for use in the pluggable cryptography @@ -88,10 +89,10 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * DhSpec spec = DhSpec.ffdhe3072(); * * // Generate a key pair using the registered builder - * KeyPair kp = CryptoAlgorithms.keyPair("DH", spec); + * KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("DH", spec); * * // Obtain an agreement context for DH key agreement - * AgreementContext ctx = CryptoAlgorithms.create("DH", KeyUsage.AGREEMENT, kp.getPrivate(), spec); + * AgreementContext ctx = session.createContext("DH", KeyUsage.AGREEMENT, kp.getPrivate(), spec); * * // Use the context with a peer public key to derive a shared secret * ctx.setPeerPublic(peerPublicKey); @@ -140,43 +141,28 @@ public final class DhAlgorithm extends AbstractCryptoAlgorithm { "DiffieHellman", null, "DH", null), DhSpec::ffdhe2048); - registerAsymmetricKeyBuilder(DhSpec.class, new DhKeyGenBuilder(), DhSpec::ffdhe2048); - registerAsymmetricKeyBuilder(DhPublicKeySpec.class, new AsymmetricKeyBuilder<>() { - - @Override - public KeyPair generateKeyPair(DhPublicKeySpec spec) throws GeneralSecurityException { - throw new UnsupportedOperationException("Use DhKeyGenBuilder for keypair generation."); - } + registerAsymmetricKeyPairGenerator(DhSpec.class, new DhKeyGenBuilder(), DhSpec::ffdhe2048); + registerPublicKeyImporter(DhPublicKeySpec.class, new PublicKeyImporter<>() { @Override public PublicKey importPublic(DhPublicKeySpec spec) throws GeneralSecurityException { KeyFactory kf = KeyFactory.getInstance("DH"); return kf.generatePublic(new X509EncodedKeySpec(spec.encoded())); } - - @Override - public PrivateKey importPrivate(DhPublicKeySpec spec) throws GeneralSecurityException { - throw new UnsupportedOperationException("Use DhPrivateKeySpec for private key import."); - } - }, null); - registerAsymmetricKeyBuilder(DhPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { - - @Override - public KeyPair generateKeyPair(DhPrivateKeySpec spec) throws GeneralSecurityException { - throw new UnsupportedOperationException("Use DhKeyGenBuilder for keypair generation."); - } - - @Override - public PublicKey importPublic(DhPrivateKeySpec spec) throws GeneralSecurityException { - throw new UnsupportedOperationException("Use DhPrivateKeySpec for public key import."); - } + }); + registerPrivateKeyImporter(DhPrivateKeySpec.class, new PrivateKeyImporter<>() { @Override public PrivateKey importPrivate(DhPrivateKeySpec spec) throws GeneralSecurityException { KeyFactory kf = KeyFactory.getInstance("DH"); - return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); + byte[] encoded = spec.encoded(); + try { + return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } finally { + Arrays.fill(encoded, (byte) 0); + } } - }, null); + }); } } diff --git a/lib/src/main/java/zeroecho/core/alg/dh/DhKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/dh/DhKeyGenBuilder.java index 42000f3..a96fedb 100644 --- a/lib/src/main/java/zeroecho/core/alg/dh/DhKeyGenBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/dh/DhKeyGenBuilder.java @@ -38,12 +38,10 @@ import java.security.AlgorithmParameters; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyPairGenerator; -import java.security.PrivateKey; -import java.security.PublicKey; import javax.crypto.spec.DHParameterSpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; /** *

      DH key pair builder

      @@ -89,7 +87,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * KeyPair kp2 = builder.generateKeyPair(sized); * } */ -public final class DhKeyGenBuilder implements AsymmetricKeyBuilder { +public final class DhKeyGenBuilder implements AsymmetricKeyPairGenerator { /** * Generates a Diffie-Hellman key pair for the given specification. * @@ -138,56 +136,4 @@ public final class DhKeyGenBuilder implements AsymmetricKeyBuilder { kpg.initialize(dh); return kpg.generateKeyPair(); } - - /** - * Unsupported for DH in this builder. - * - *

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

      - * - *

      - * Example - *

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

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

      - * - *

      - * Example - *

      - *
      {@code
      -     * // This will throw UnsupportedOperationException
      -     * new DhKeyGenBuilder().importPrivate(DhSpec.ffdhe2048());
      -     * }
      - * - * @param spec the DH specification (ignored) - * @return never returns normally - * @throws UnsupportedOperationException always thrown - */ - @Override - public PrivateKey importPrivate(DhSpec spec) { - throw new UnsupportedOperationException(); - } } diff --git a/lib/src/main/java/zeroecho/core/alg/dh/DhPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/dh/DhPrivateKeySpec.java index b917dc3..03fdd75 100644 --- a/lib/src/main/java/zeroecho/core/alg/dh/DhPrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/dh/DhPrivateKeySpec.java @@ -33,7 +33,11 @@ ******************************************************************************/ package zeroecho.core.alg.dh; +import java.util.Arrays; import java.util.Base64; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.spec.AlgorithmKeySpec; @@ -50,8 +54,9 @@ import zeroecho.core.spec.AlgorithmKeySpec; * *

      Design

      *
        - *
      • Immutable: the internal byte array is defensively copied at construction - * and when returned by {@link #encoded()}.
      • + *
      • Destroyable: the internal byte array is defensively copied at + * construction and when returned by {@link #encoded()}, and is cleared by + * {@link #destroy()}.
      • *
      • Encodable: supports marshaling to/from a * {@link zeroecho.core.marshal.PairSeq} so keys can be serialized in * human-readable or protocol-friendly formats.
      • @@ -62,7 +67,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; *

        Example

        {@code
          * // Import a DH private key from encoded bytes
          * DhPrivateKeySpec spec = new DhPrivateKeySpec(pkcs8Bytes);
        - * PrivateKey priv = CryptoAlgorithms.privateKey("DH", spec);
        + * PrivateKey priv = session.keyBuilders().asymmetric().importPrivate("DH", spec);
          *
          * // Marshal to a text-friendly representation
          * PairSeq ps = DhPrivateKeySpec.marshal(spec);
        @@ -73,10 +78,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
          *
          * @since 1.0
          */
        -public class DhPrivateKeySpec implements AlgorithmKeySpec {
        +public class DhPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
         
             private static final String PKCS8_B64 = "pkcs8.b64";
             private final byte[] pkcs8;
        +    private final ReentrantLock lifecycleLock = new ReentrantLock();
        +    private boolean destroyed;
         
             /**
              * Creates a new specification from a PKCS#8 encoded DH private key.
        @@ -97,7 +104,13 @@ public class DhPrivateKeySpec implements AlgorithmKeySpec {
              * @return a defensive copy of the PKCS#8 encoded DH private key
              */
             public byte[] encoded() {
        -        return pkcs8.clone();
        +        lifecycleLock.lock();
        +        try {
        +            ensureActive();
        +            return pkcs8.clone();
        +        } finally {
        +            lifecycleLock.unlock();
        +        }
             }
         
             /**
        @@ -113,7 +126,7 @@ public class DhPrivateKeySpec implements AlgorithmKeySpec {
              * @throws NullPointerException if {@code spec} is {@code null}
              */
             public static PairSeq marshal(DhPrivateKeySpec spec) {
        -        String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
        +        String b64 = spec.encodedKey();
                 return PairSeq.of("type", "DH-PRIV", PKCS8_B64, b64);
             }
         
        @@ -137,12 +150,62 @@ public class DhPrivateKeySpec implements AlgorithmKeySpec {
                     String k = cur.key();
                     String v = cur.value();
                     if (PKCS8_B64.equals(k)) {
        -                out = Base64.getDecoder().decode(v);
        +                out = decodeReplacing(out, v);
                     }
                 }
                 if (out == null) {
                     throw new IllegalArgumentException("pkcs8.b64 missing for DH private key");
                 }
        -        return new DhPrivateKeySpec(out);
        +        try {
        +            return new DhPrivateKeySpec(out);
        +        } finally {
        +            Arrays.fill(out, (byte) 0);
        +        }
        +    }
        +
        +    private static byte[] decodeReplacing(byte[] current, String encoded) {
        +        if (current != null) {
        +            Arrays.fill(current, (byte) 0);
        +        }
        +        return Base64.getDecoder().decode(encoded);
        +    }
        +
        +    private String encodedKey() {
        +        lifecycleLock.lock();
        +        try {
        +            ensureActive();
        +            return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
        +        } finally {
        +            lifecycleLock.unlock();
        +        }
        +    }
        +
        +    @Override
        +    public void destroy() {
        +        lifecycleLock.lock();
        +        try {
        +            if (!destroyed) {
        +                Arrays.fill(pkcs8, (byte) 0);
        +                destroyed = true;
        +            }
        +        } finally {
        +            lifecycleLock.unlock();
        +        }
        +    }
        +
        +    @Override
        +    public boolean isDestroyed() {
        +        lifecycleLock.lock();
        +        try {
        +            return destroyed;
        +        } finally {
        +            lifecycleLock.unlock();
        +        }
        +    }
        +
        +    private void ensureActive() {
        +        if (destroyed) {
        +            throw new IllegalStateException("DH private key specification has been destroyed");
        +        }
             }
         }
        diff --git a/lib/src/main/java/zeroecho/core/alg/dh/DhPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/dh/DhPublicKeySpec.java
        index 5698124..6ed5966 100644
        --- a/lib/src/main/java/zeroecho/core/alg/dh/DhPublicKeySpec.java
        +++ b/lib/src/main/java/zeroecho/core/alg/dh/DhPublicKeySpec.java
        @@ -64,7 +64,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
          * 

        Example

        {@code
          * // Import a DH public key from encoded bytes
          * DhPublicKeySpec spec = new DhPublicKeySpec(x509Bytes);
        - * PublicKey pub = CryptoAlgorithms.publicKey("DH", spec);
        + * PublicKey pub = session.keyBuilders().asymmetric().importPublic("DH", spec);
          *
          * // Marshal to a text-friendly representation
          * PairSeq ps = DhPublicKeySpec.marshal(spec);
        diff --git a/lib/src/main/java/zeroecho/core/alg/dh/DhSpec.java b/lib/src/main/java/zeroecho/core/alg/dh/DhSpec.java
        index 0610c6f..7d9ff2f 100644
        --- a/lib/src/main/java/zeroecho/core/alg/dh/DhSpec.java
        +++ b/lib/src/main/java/zeroecho/core/alg/dh/DhSpec.java
        @@ -80,10 +80,10 @@ import zeroecho.core.spec.ContextSpec;
          *
          * 

        Example

        {@code
          * // Create a key pair in the FFDHE-3072 group
        - * KeyPair kp = CryptoAlgorithms.keyPair("DH", DhSpec.ffdhe3072());
        + * KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("DH", DhSpec.ffdhe3072());
          *
          * // Establish an agreement context
        - * AgreementContext ctx = CryptoAlgorithms.create(
        + * AgreementContext ctx = session.createContext(
          *     "DH", KeyUsage.AGREEMENT, kp.getPrivate(), DhSpec.ffdhe3072());
          * }
        * diff --git a/lib/src/main/java/zeroecho/core/alg/dh/package-info.java b/lib/src/main/java/zeroecho/core/alg/dh/package-info.java index 0cfe37e..60695dc 100644 --- a/lib/src/main/java/zeroecho/core/alg/dh/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/dh/package-info.java @@ -51,8 +51,8 @@ * ad-hoc parameter generation. *
      • Expose predefined RFC 7919 FFDHE groups for safe parameter selection. *
      • - *
      • Allow import/export of encoded keys via immutable key specs supporting - * PKCS#8 and X.509.
      • + *
      • Allow import/export of encoded keys via defensively copying key specs + * supporting PKCS#8 and X.509; private-key specs are destroyable.
      • *
      * *

      Components

      @@ -65,9 +65,10 @@ * {@link javax.crypto.spec.DHParameterSpec} instances. *
    14. DhSpec: immutable container for DH parameters; provides static * factories for FFDHE groups (2048–8192 bits).
    15. - *
    16. DhPublicKeySpec and DhPrivateKeySpec: immutable encoded key - * specs for importing/exporting X.509 and PKCS#8 encodings, with - * {@link zeroecho.core.marshal.PairSeq} marshalling support.
    17. + *
    18. DhPublicKeySpec and DhPrivateKeySpec: encoded key specs for + * importing/exporting X.509 and PKCS#8 encodings, with + * {@link zeroecho.core.marshal.PairSeq} marshalling support; the private-key + * form is destroyable.
    19. * * *

      Design notes

      diff --git a/lib/src/main/java/zeroecho/core/alg/digest/DigestSpec.java b/lib/src/main/java/zeroecho/core/alg/digest/DigestSpec.java index 319512d..4805d6d 100644 --- a/lib/src/main/java/zeroecho/core/alg/digest/DigestSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/digest/DigestSpec.java @@ -76,7 +76,7 @@ import zeroecho.core.spec.ContextSpec; * DigestSpec spec = DigestSpec.shake256(64); * * // Use in context creation - * DigestContext ctx = CryptoAlgorithms.create( + * DigestContext ctx = session.createContext( * "DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, spec); * * byte[] digest = ctx.doFinal(data); diff --git a/lib/src/main/java/zeroecho/core/alg/digest/Sha2Sha3Algorithm.java b/lib/src/main/java/zeroecho/core/alg/digest/Sha2Sha3Algorithm.java index 54661d0..3677b26 100644 --- a/lib/src/main/java/zeroecho/core/alg/digest/Sha2Sha3Algorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/digest/Sha2Sha3Algorithm.java @@ -33,7 +33,6 @@ ******************************************************************************/ package zeroecho.core.alg.digest; -import java.io.IOException; import java.security.GeneralSecurityException; import java.security.MessageDigest; @@ -43,6 +42,7 @@ import zeroecho.core.KeyUsage; import zeroecho.core.NullKey; import zeroecho.core.alg.AbstractCryptoAlgorithm; import zeroecho.core.context.DigestContext; +import zeroecho.core.err.ProviderFailureException; /** *

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

      @@ -77,7 +77,7 @@ import zeroecho.core.context.DigestContext; * CryptoAlgorithm algo = CryptoAlgorithms.require("DIGEST"); * * // Create a digest context for SHA3-512 - * DigestContext ctx = CryptoAlgorithms.create( + * DigestContext ctx = session.createContext( * "DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, DigestSpec.sha3_512()); * * // Stream data into the digest @@ -117,7 +117,8 @@ public final class Sha2Sha3Algorithm extends AbstractCryptoAlgorithm { MessageDigest md = MessageDigest.getInstance(s.algorithm().jca()); return new JcaDigestContext(this, md, s); } catch (GeneralSecurityException e) { - throw new IOException("Failed to init MessageDigest: " + s.algorithm().jca(), e); + throw new ProviderFailureException( + "Failed to initialize MessageDigest " + s.algorithm().jca(), e); } }, DigestSpec::sha256 // default for catalog/tests ); diff --git a/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhAlgorithm.java index 8590fb7..087e4ac 100644 --- a/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhAlgorithm.java @@ -90,18 +90,18 @@ import zeroecho.core.context.MessageAgreementContext; * *

      Example

      {@code
        * // Generate a key pair for Alice
      - * KeyPair aliceKeys = CryptoAlgorithms.generateKeyPair("ECDH", EcdhCurveSpec.P256);
      + * KeyPair aliceKeys = session.keyBuilders().asymmetric().generateKeyPair("ECDH", EcdhCurveSpec.P256);
        *
        * // Generate a key pair for Bob
      - * KeyPair bobKeys = CryptoAlgorithms.generateKeyPair("ECDH", EcdhCurveSpec.P256);
      + * KeyPair bobKeys = session.keyBuilders().asymmetric().generateKeyPair("ECDH", EcdhCurveSpec.P256);
        *
        * // Alice computes shared secret using her private key
      - * AgreementContext aliceCtx = CryptoAlgorithms.create("ECDH",
      + * AgreementContext aliceCtx = session.createContext("ECDH",
        *         KeyUsage.AGREEMENT, aliceKeys.getPrivate(), EcdsaCurveSpec.P256);
        * byte[] aliceSecret = aliceCtx.derive(bobKeys.getPublic());
        *
        * // Bob computes shared secret using his private key
      - * AgreementContext bobCtx = CryptoAlgorithms.create("ECDH",
      + * AgreementContext bobCtx = session.createContext("ECDH",
        *         KeyUsage.AGREEMENT, bobKeys.getPrivate(), EcdsaCurveSpec.P256);
        * byte[] bobSecret = bobCtx.derive(aliceKeys.getPublic());
        *
      @@ -160,8 +160,9 @@ public final class EcdhAlgorithm extends AbstractCryptoAlgorithm {
                       () -> EcdsaCurveSpec.P256);
       
               // Reuse EC builders/importers
      -        registerAsymmetricKeyBuilder(EcdhCurveSpec.class, new EcdhKeyGenBuilder(), () -> EcdhCurveSpec.P256);
      -        registerAsymmetricKeyBuilder(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder(), null);
      -        registerAsymmetricKeyBuilder(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder(), null);
      +        registerAsymmetricKeyPairGenerator(EcdhCurveSpec.class, new EcdhKeyGenBuilder(),
      +                () -> EcdhCurveSpec.P256);
      +        registerPublicKeyImporter(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder());
      +        registerPrivateKeyImporter(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder());
           }
       }
      diff --git a/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhCurveSpec.java b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhCurveSpec.java
      index e6179aa..bb2e9a9 100644
      --- a/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhCurveSpec.java
      +++ b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhCurveSpec.java
      @@ -63,7 +63,7 @@ import zeroecho.core.spec.ContextSpec;
        *
        * 

      Usage

      {@code
        * // Generate a key pair on P-256
      - * KeyPair kp = CryptoAlgorithms.generateKeyPair("ECDH", EcdhCurveSpec.P256);
      + * KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("ECDH", EcdhCurveSpec.P256);
        *
        * // Use curve metadata
        * String jcaName = EcdhCurveSpec.P256.curveName(); // "secp256r1"
      diff --git a/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhKeyGenBuilder.java
      index df79109..451ac69 100644
      --- a/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhKeyGenBuilder.java
      +++ b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhKeyGenBuilder.java
      @@ -39,15 +39,13 @@ import java.security.KeyPairGenerator;
       import java.security.spec.ECGenParameterSpec;
       
       import zeroecho.core.alg.ecdsa.EcdsaPrivateKeyBuilder;
      -import zeroecho.core.alg.ecdsa.EcdsaPrivateKeySpec;
       import zeroecho.core.alg.ecdsa.EcdsaPublicKeyBuilder;
      -import zeroecho.core.alg.ecdsa.EcdsaPublicKeySpec;
      -import zeroecho.core.spi.AsymmetricKeyBuilder;
      +import zeroecho.core.spi.AsymmetricKeyPairGenerator;
       
       /**
        * 

      ECDH Key Pair Generator

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

      @@ -70,7 +68,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * * @since 1.0 */ -public final class EcdhKeyGenBuilder implements AsymmetricKeyBuilder { +public final class EcdhKeyGenBuilder implements AsymmetricKeyPairGenerator { /** * Generates a new elliptic curve key pair for use in ECDH key agreement. * @@ -92,40 +90,4 @@ public final class EcdhKeyGenBuilder implements AsymmetricKeyBuilder - * Importing existing ECDH public keys should be performed via - * {@link EcdsaPublicKeyBuilder} with an {@link EcdsaPublicKeySpec}. This method - * will always throw an {@link UnsupportedOperationException}. - *

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

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

      - * - * @param spec ignored - * @return never returns normally - * @throws UnsupportedOperationException always - */ - @Override - public java.security.PrivateKey importPrivate(EcdhCurveSpec spec) { - throw new UnsupportedOperationException("Use EcdhPrivateKeySpec with EcdsaPrivateKeyBuilder."); - } } diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaAlgorithm.java index 6c7a1df..797630f 100644 --- a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaAlgorithm.java @@ -39,7 +39,6 @@ import java.security.PublicKey; import zeroecho.core.AlgorithmFamily; import zeroecho.core.CryptoAlgorithm; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.CryptoCatalog; import zeroecho.core.KeyUsage; import zeroecho.core.alg.AbstractCryptoAlgorithm; @@ -82,9 +81,9 @@ import zeroecho.core.context.SignatureContext; * *
      {@code
        * // Example: Sign and verify with ECDSA/P-256
      - * KeyPair kp = CryptoAlgorithms.keyPair("ECDSA", EcdsaCurveSpec.P256);
      - * SignatureContext signer = CryptoAlgorithms.create("ECDSA", KeyUsage.SIGN, kp.getPrivate(), EcdsaCurveSpec.P256);
      - * SignatureContext verifier = CryptoAlgorithms.create("ECDSA", KeyUsage.VERIFY, kp.getPublic(), EcdsaCurveSpec.P256);
      + * KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("ECDSA", EcdsaCurveSpec.P256);
      + * SignatureContext signer = session.createContext("ECDSA", KeyUsage.SIGN, kp.getPrivate(), EcdsaCurveSpec.P256);
      + * SignatureContext verifier = session.createContext("ECDSA", KeyUsage.VERIFY, kp.getPublic(), EcdsaCurveSpec.P256);
        * }
      * * @since 1.0 @@ -104,8 +103,8 @@ public final class EcdsaAlgorithm extends AbstractCryptoAlgorithm { *

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

      */ public EcdsaAlgorithm() { @@ -135,8 +134,9 @@ public final class EcdsaAlgorithm extends AbstractCryptoAlgorithm { } }, () -> EcdsaCurveSpec.P256); - registerAsymmetricKeyBuilder(EcdsaCurveSpec.class, new EcdsaKeyGenBuilder(), () -> EcdsaCurveSpec.P256); - registerAsymmetricKeyBuilder(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder(), null); - registerAsymmetricKeyBuilder(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder(), null); + registerAsymmetricKeyPairGenerator(EcdsaCurveSpec.class, new EcdsaKeyGenBuilder(), + () -> EcdsaCurveSpec.P256); + registerPublicKeyImporter(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder()); + registerPrivateKeyImporter(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder()); } } diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaKeyGenBuilder.java index c759fb7..7f628b5 100644 --- a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaKeyGenBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaKeyGenBuilder.java @@ -39,30 +39,23 @@ import java.security.KeyPairGenerator; import java.security.spec.ECGenParameterSpec; import zeroecho.core.CryptoAlgorithm; -import zeroecho.core.CryptoAlgorithms; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; /** *

      ECDSA Key Pair Generator

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

      Supported operations

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

      The exact supported operation is + * {@link #generateKeyPair(EcdsaCurveSpec)}. Public and private import are + * registered separately through {@link EcdsaPublicKeyBuilder} and + * {@link EcdsaPrivateKeyBuilder}.

      * - *

      Usage

      Typically accessed indirectly through - * {@link CryptoAlgorithms#keyPair(String, zeroecho.core.spec.AlgorithmKeySpec)} - * or - * {@link CryptoAlgorithm#generateKeyPair(zeroecho.core.spec.AlgorithmKeySpec)}. + *

      Usage

      Typically accessed through the session key-operation API or + * {@link CryptoAlgorithm#asymmetricKeyPairGenerator(Class)}. * *
      {@code
        * // Example: Generate an ECDSA P-256 key pair
      @@ -72,7 +65,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
        *
        * @since 1.0
        */
      -public final class EcdsaKeyGenBuilder implements AsymmetricKeyBuilder {
      +public final class EcdsaKeyGenBuilder implements AsymmetricKeyPairGenerator {
           /**
            * Generates a new elliptic curve key pair for the given curve specification.
            *
      @@ -93,38 +86,4 @@ public final class EcdsaKeyGenBuilder implements AsymmetricKeyBuilder
      -     * Public key import should be performed using {@link EcdsaPublicKeySpec} and
      -     * {@link EcdsaPublicKeyBuilder}.
      -     * 

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

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

      - * - * @param spec unused curve specification - * @return never returns normally - * @throws UnsupportedOperationException always thrown - */ - @Override - public java.security.PrivateKey importPrivate(EcdsaCurveSpec spec) { - throw new UnsupportedOperationException("Use EcdsaPrivateKeySpec with EcdsaPrivateKeyBuilder."); - } } diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPrivateKeyBuilder.java index a9e76b8..ac05027 100644 --- a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPrivateKeyBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPrivateKeyBuilder.java @@ -37,36 +37,28 @@ import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; import zeroecho.core.CryptoAlgorithm; -import zeroecho.core.CryptoAlgorithms; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.PrivateKeyImporter; /** *

      ECDSA Private Key Builder

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

      Supported operations

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

      The exact supported operation is + * {@link #importPrivate(EcdsaPrivateKeySpec)}. Generation and public import are + * registered through their own operation-specific implementations.

      * *

      Encoding

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

      Usage

      Typically accessed indirectly through - * {@link CryptoAlgorithms#privateKey(String, zeroecho.core.spec.AlgorithmKeySpec)} - * or - * {@link CryptoAlgorithm#importPrivate(zeroecho.core.spec.AlgorithmKeySpec)}. + *

      Usage

      Typically accessed through the session key-operation API or + * {@link CryptoAlgorithm#privateKeyImporter(Class)}. * *
      {@code
        * // Example: Import an ECDSA private key
      @@ -77,40 +69,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
        *
        * @since 1.0
        */
      -public final class EcdsaPrivateKeyBuilder implements AsymmetricKeyBuilder {
      -    /**
      -     * Unsupported operation for this builder.
      -     *
      -     * 

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

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

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

      - * - * @param spec unused private key specification - * @return never returns normally - * @throws UnsupportedOperationException always thrown - */ - @Override - public java.security.PublicKey importPublic(EcdsaPrivateKeySpec spec) { - throw new UnsupportedOperationException("Use EcdsaPublicKeySpec with EcdsaPublicKeyBuilder."); - } +public final class EcdsaPrivateKeyBuilder implements PrivateKeyImporter { /** * Imports a private key from a PKCS#8 encoded specification. @@ -128,6 +87,11 @@ public final class EcdsaPrivateKeyBuilder implements AsymmetricKeyBuilderECDSA Private Key Specification * - * An immutable wrapper around a PKCS#8-encoded ECDSA private key. This + * A destroyable wrapper around a PKCS#8-encoded ECDSA private key. This * specification is used by {@link EcdsaPrivateKeyBuilder} to import keys into * the JCA {@link java.security.PrivateKey} representation. * @@ -69,10 +73,12 @@ import zeroecho.core.spec.AlgorithmKeySpec; * * @since 1.0 */ -public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec { +public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] pkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Creates a new private key specification from a PKCS#8 encoded byte array. @@ -93,7 +99,13 @@ public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec { * @return cloned PKCS#8 byte array */ public byte[] encoded() { - return pkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return pkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -108,7 +120,7 @@ public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec { * @return serialized representation in key-value form */ public static PairSeq marshal(EcdsaPrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "ECDSA-PRIV", PKCS8_B64, b64); } @@ -131,12 +143,62 @@ public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec { String k = cur.key(); String v = cur.value(); if (PKCS8_B64.equals(k)) { - out = Base64.getDecoder().decode(v); + out = decodeReplacing(out, v); } } if (out == null) { throw new IllegalArgumentException("pkcs8.b64 missing for ECDSA private key"); } - return new EcdsaPrivateKeySpec(out); + try { + return new EcdsaPrivateKeySpec(out); + } finally { + Arrays.fill(out, (byte) 0); + } + } + + private static byte[] decodeReplacing(byte[] current, String encoded) { + if (current != null) { + Arrays.fill(current, (byte) 0); + } + return Base64.getDecoder().decode(encoded); + } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(pkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(pkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("ECDSA private key specification has been destroyed"); + } } } diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPublicKeyBuilder.java index 47f1390..c67dc3f 100644 --- a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPublicKeyBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPublicKeyBuilder.java @@ -39,33 +39,25 @@ import java.security.PublicKey; import java.security.spec.X509EncodedKeySpec; import zeroecho.core.CryptoAlgorithm; -import zeroecho.core.CryptoAlgorithms; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.PublicKeyImporter; /** *

      ECDSA Public Key Builder

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

      Supported operations

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

      The exact supported operation is + * {@link #importPublic(EcdsaPublicKeySpec)}. Generation and private import are + * registered through their own operation-specific implementations.

      * *

      Encoding

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

      Usage

      Typically accessed indirectly through - * {@link CryptoAlgorithms#publicKey(String, zeroecho.core.spec.AlgorithmKeySpec)} - * or {@link CryptoAlgorithm#importPublic(zeroecho.core.spec.AlgorithmKeySpec)}. + *

      Usage

      Typically accessed through the session key-operation API or + * {@link CryptoAlgorithm#publicKeyImporter(Class)}. * *
      {@code
        * // Example: Import an ECDSA public key
      @@ -76,23 +68,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
        *
        * @since 1.0
        */
      -public final class EcdsaPublicKeyBuilder implements AsymmetricKeyBuilder {
      -    /**
      -     * Unsupported operation for this builder.
      -     *
      -     * 

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

      - * - * @param spec unused public key specification - * @return never returns normally - * @throws UnsupportedOperationException always thrown - */ - @Override - public java.security.KeyPair generateKeyPair(EcdsaPublicKeySpec spec) { - throw new UnsupportedOperationException("Use EcdsaKeyGenBuilder for keypair generation."); - } +public final class EcdsaPublicKeyBuilder implements PublicKeyImporter { /** * Imports a public key from an X.509 SubjectPublicKeyInfo specification. @@ -112,21 +88,4 @@ public final class EcdsaPublicKeyBuilder implements AsymmetricKeyBuilder - * Private key import should be performed using {@link EcdsaPrivateKeySpec} with - * {@link EcdsaPrivateKeyBuilder}. - *

      - * - * @param spec unused public key specification - * @return never returns normally - * @throws UnsupportedOperationException always thrown - */ - @Override - public java.security.PrivateKey importPrivate(EcdsaPublicKeySpec spec) { - throw new UnsupportedOperationException("Use EcdsaPrivateKeySpec with EcdsaPrivateKeyBuilder."); - } } diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/package-info.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/package-info.java index 715f765..1a71892 100644 --- a/lib/src/main/java/zeroecho/core/alg/ecdsa/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/package-info.java @@ -36,9 +36,9 @@ * *

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

      * *

      Scope and responsibilities

      @@ -66,8 +66,9 @@ *
    20. EcdsaPublicKeyBuilder and EcdsaPrivateKeyBuilder: import * keys from X.509 and PKCS#8 encodings via * {@link java.security.KeyFactory}.
    21. - *
    22. EcdsaPublicKeySpec and EcdsaPrivateKeySpec: immutable - * wrappers around encoded keys with marshalling support.
    23. + *
    24. EcdsaPublicKeySpec and EcdsaPrivateKeySpec: wrappers around + * encoded keys with marshalling support; the private-key form is + * destroyable.
    25. * * *

      Design notes

      diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519Algorithm.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519Algorithm.java index f4116fc..674665b 100644 --- a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519Algorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519Algorithm.java @@ -122,9 +122,9 @@ public final class Ed25519Algorithm extends AbstractCryptoAlgorithm { }, () -> VoidSpec.INSTANCE); // Key builders - registerAsymmetricKeyBuilder(Ed25519KeyGenSpec.class, new Ed25519KeyGenBuilder(), + registerAsymmetricKeyPairGenerator(Ed25519KeyGenSpec.class, new Ed25519KeyGenBuilder(), Ed25519KeyGenSpec::defaultSpec); - registerAsymmetricKeyBuilder(Ed25519PublicKeySpec.class, new Ed25519PublicKeyBuilder(), null); - registerAsymmetricKeyBuilder(Ed25519PrivateKeySpec.class, new Ed25519PrivateKeyBuilder(), null); + registerPublicKeyImporter(Ed25519PublicKeySpec.class, new Ed25519PublicKeyBuilder()); + registerPrivateKeyImporter(Ed25519PrivateKeySpec.class, new Ed25519PrivateKeyBuilder()); } } diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenBuilder.java index 7a2e9e7..50a11bc 100644 --- a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenBuilder.java @@ -38,7 +38,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEdDSAKeyGenBuilder; /** *

      Key-pair builder for Ed25519

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

      @@ -50,7 +50,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEdDSAKeyGenBuilder; *

      Usage example

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

      Thread-safety

      Instances of this builder are stateless and may be diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenSpec.java index bf4d87c..4555f72 100644 --- a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenSpec.java @@ -50,7 +50,8 @@ import zeroecho.core.spec.AlgorithmKeySpec; * *

      Usage example

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

      Thread-safety

      The default spec instance is immutable and safe to diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeyBuilder.java index 63b5e9f..8e324f0 100644 --- a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeyBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeyBuilder.java @@ -38,7 +38,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder; /** *

      Private key builder for Ed25519

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

      @@ -59,7 +59,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder; *

      Usage example

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

      Thread-safety

      Instances of this builder are stateless and may be diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeySpec.java index 15dfdc7..365d23e 100644 --- a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeySpec.java @@ -33,7 +33,11 @@ ******************************************************************************/ package zeroecho.core.alg.ed25519; +import java.util.Arrays; import java.util.Base64; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.spec.AlgorithmKeySpec; @@ -65,7 +69,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * Ed25519PrivateKeySpec spec = new Ed25519PrivateKeySpec(pkcs8Bytes); * * // Import into a PrivateKey using ZeroEcho - * PrivateKey priv = CryptoAlgorithms.privateKey("Ed25519", spec); + * PrivateKey priv = session.keyBuilders().asymmetric().importPrivate("Ed25519", spec); * * // Serialize to PairSeq (e.g., for configuration or transport) * PairSeq seq = Ed25519PrivateKeySpec.marshal(spec); @@ -74,16 +78,17 @@ import zeroecho.core.spec.AlgorithmKeySpec; * Ed25519PrivateKeySpec restored = Ed25519PrivateKeySpec.unmarshal(seq); * }
      * - *

      Thread-safety

      Instances are immutable. The internal key bytes are - * defensively copied on construction and retrieval, making this class safe to - * share across threads. + *

      Thread-safety

      Access and destruction are synchronized. The internal + * key bytes are defensively copied on construction and retrieval. * * @since 1.0 */ -public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec { +public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] encodedPkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Creates a new Ed25519 private key specification from its PKCS#8 encoding. @@ -104,7 +109,13 @@ public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec { * @return clone of the PKCS#8 encoding */ public byte[] encoded() { - return encodedPkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return encodedPkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -119,7 +130,7 @@ public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec { * @return serialized representation as a {@link PairSeq} */ public static PairSeq marshal(Ed25519PrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "Ed25519-PRIV", PKCS8_B64, b64); } @@ -143,12 +154,62 @@ public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec { String k = cur.key(); String v = cur.value(); if (PKCS8_B64.equals(k)) { - out = Base64.getDecoder().decode(v); + out = decodeReplacing(out, v); } } if (out == null) { throw new IllegalArgumentException("pkcs8.b64 missing for Ed25519 private key"); } - return new Ed25519PrivateKeySpec(out); + try { + return new Ed25519PrivateKeySpec(out); + } finally { + Arrays.fill(out, (byte) 0); + } + } + + private static byte[] decodeReplacing(byte[] current, String encoded) { + if (current != null) { + Arrays.fill(current, (byte) 0); + } + return Base64.getDecoder().decode(encoded); + } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(encodedPkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(encodedPkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("Ed25519 private key specification has been destroyed"); + } } } diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeyBuilder.java index e335361..886117e 100644 --- a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeyBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeyBuilder.java @@ -38,7 +38,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder; /** *

      Public key builder for Ed25519

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

      @@ -59,7 +59,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder; *

      Usage example

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

      Thread-safety

      Instances of this builder are stateless and may be diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeySpec.java index c32908d..3a34122 100644 --- a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeySpec.java @@ -65,7 +65,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * Ed25519PublicKeySpec spec = new Ed25519PublicKeySpec(x509Bytes); * * // Import into a PublicKey using ZeroEcho - * PublicKey pub = CryptoAlgorithms.publicKey("Ed25519", spec); + * PublicKey pub = session.keyBuilders().asymmetric().importPublic("Ed25519", spec); * * // Serialize to PairSeq (e.g., for configuration or transport) * PairSeq seq = Ed25519PublicKeySpec.marshal(spec); diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/package-info.java b/lib/src/main/java/zeroecho/core/alg/ed25519/package-info.java index 22b1961..cb8021c 100644 --- a/lib/src/main/java/zeroecho/core/alg/ed25519/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/package-info.java @@ -49,8 +49,8 @@ * enforces the 64-byte tag size. *
    26. Expose builders for key-pair generation and for importing encoded * public/private keys.
    27. - *
    28. Define immutable key specifications suitable for safe cloning and simple - * marshalling.
    29. + *
    30. Define defensively copying key specifications suitable for safe cloning + * and simple marshalling; private-key specifications are destroyable.
    31. * * *

      Components

      @@ -63,9 +63,9 @@ * marker spec for producing key pairs. *
    32. Ed25519PublicKeyBuilder / Ed25519PrivateKeyBuilder: * importers backed by JCA key factories.
    33. - *
    34. Ed25519PublicKeySpec / Ed25519PrivateKeySpec: immutable - * wrappers over X.509 and PKCS#8 encodings, with defensive copying and simple - * base64 marshalling helpers.
    35. + *
    36. Ed25519PublicKeySpec / Ed25519PrivateKeySpec: wrappers over + * X.509 and PKCS#8 encodings, with defensive copying and simple base64 + * marshalling helpers; the private-key form is destroyable.
    37. * * *

      Design notes

      diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448Algorithm.java b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448Algorithm.java index 0e6259b..e598b02 100644 --- a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448Algorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448Algorithm.java @@ -82,12 +82,13 @@ import zeroecho.core.spec.VoidSpec; *
      {@code
        * // Example: generate a key pair and sign data
        * CryptoAlgorithm ed448 = new Ed448Algorithm();
      - * KeyPair kp = ed448.generateKeyPair(Ed448KeyGenSpec.defaultSpec());
      + * KeyPair kp = ed448.asymmetricKeyPairGenerator(Ed448KeyGenSpec.class)
      + *                    .generateKeyPair(Ed448KeyGenSpec.defaultSpec());
        *
      - * SignatureContext signer = ed448.create(KeyUsage.SIGN, kp.getPrivate(), null);
      + * SignatureContext signer = ed448.createContext(KeyUsage.SIGN, kp.getPrivate(), null);
        * byte[] sig = signer.sign(data);
        *
      - * SignatureContext verifier = ed448.create(KeyUsage.VERIFY, kp.getPublic(), null);
      + * SignatureContext verifier = ed448.createContext(KeyUsage.VERIFY, kp.getPublic(), null);
        * boolean ok = verifier.verify(data, sig);
        * }
      * @@ -128,7 +129,7 @@ public final class Ed448Algorithm extends AbstractCryptoAlgorithm { *
      {@code
            * // Example: instantiate and obtain a signer
            * CryptoAlgorithm ed448 = new Ed448Algorithm();
      -     * SignatureContext signer = ed448.create(KeyUsage.SIGN, privateKey, null);
      +     * SignatureContext signer = ed448.createContext(KeyUsage.SIGN, privateKey, null);
            * }
      */ public Ed448Algorithm() { @@ -155,8 +156,9 @@ public final class Ed448Algorithm extends AbstractCryptoAlgorithm { }, () -> VoidSpec.INSTANCE); // Key builders - registerAsymmetricKeyBuilder(Ed448KeyGenSpec.class, new Ed448KeyGenBuilder(), Ed448KeyGenSpec::defaultSpec); - registerAsymmetricKeyBuilder(Ed448PublicKeySpec.class, new Ed448PublicKeyBuilder(), null); - registerAsymmetricKeyBuilder(Ed448PrivateKeySpec.class, new Ed448PrivateKeyBuilder(), null); + registerAsymmetricKeyPairGenerator(Ed448KeyGenSpec.class, new Ed448KeyGenBuilder(), + Ed448KeyGenSpec::defaultSpec); + registerPublicKeyImporter(Ed448PublicKeySpec.class, new Ed448PublicKeyBuilder()); + registerPrivateKeyImporter(Ed448PrivateKeySpec.class, new Ed448PrivateKeyBuilder()); } } diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeyBuilder.java index cbd1a9d..883339f 100644 --- a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeyBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeyBuilder.java @@ -35,7 +35,6 @@ package zeroecho.core.alg.ed448; import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder; import zeroecho.core.spec.AlgorithmKeySpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; /** *

      Ed448 Private Key Builder

      @@ -66,8 +65,8 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * }
      * *

      Thread-safety

      Stateless and safe for concurrent use. Each call to - * {@link AsymmetricKeyBuilder#importPrivate(AlgorithmKeySpec) - * importPrivate(Ed448PrivateKeySpec)} creates a new + * {@link zeroecho.core.spi.PrivateKeyImporter#importPrivate(AlgorithmKeySpec)} + * creates a new * {@link java.security.KeyFactory}. * * @since 1.0 diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeySpec.java index b216f00..f42c000 100644 --- a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeySpec.java @@ -33,7 +33,11 @@ ******************************************************************************/ package zeroecho.core.alg.ed448; +import java.util.Arrays; import java.util.Base64; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.spec.AlgorithmKeySpec; @@ -41,7 +45,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; /** *

      Ed448 Private Key Specification

      * - * Immutable specification for an Ed448 private key in PKCS#8 encoding. + * Destroyable specification for an Ed448 private key in PKCS#8 encoding. * *

      * This class acts as a typed carrier for encoded private key material, @@ -74,15 +78,16 @@ import zeroecho.core.spec.AlgorithmKeySpec; * Ed448PrivateKeySpec restored = Ed448PrivateKeySpec.unmarshal(p); * }

      * - *

      Thread-safety

      Instances are immutable and safe to share across - * threads. + *

      Thread-safety

      Access and destruction are synchronized. * * @since 1.0 */ -public final class Ed448PrivateKeySpec implements AlgorithmKeySpec { +public final class Ed448PrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] encodedPkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Constructs a new Ed448 private key spec from the given PKCS#8-encoded bytes. @@ -103,7 +108,13 @@ public final class Ed448PrivateKeySpec implements AlgorithmKeySpec { * @return cloned PKCS#8-encoded key bytes */ public byte[] encoded() { - return encodedPkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return encodedPkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -119,7 +130,7 @@ public final class Ed448PrivateKeySpec implements AlgorithmKeySpec { * @return a {@link PairSeq} containing the type and base64 data */ public static PairSeq marshal(Ed448PrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "Ed448-PRIV", PKCS8_B64, b64); } @@ -142,12 +153,62 @@ public final class Ed448PrivateKeySpec implements AlgorithmKeySpec { String k = cur.key(); String v = cur.value(); if (PKCS8_B64.equals(k)) { - out = Base64.getDecoder().decode(v); + out = decodeReplacing(out, v); } } if (out == null) { throw new IllegalArgumentException("pkcs8.b64 missing for Ed448 private key"); } - return new Ed448PrivateKeySpec(out); + try { + return new Ed448PrivateKeySpec(out); + } finally { + Arrays.fill(out, (byte) 0); + } + } + + private static byte[] decodeReplacing(byte[] current, String encoded) { + if (current != null) { + Arrays.fill(current, (byte) 0); + } + return Base64.getDecoder().decode(encoded); + } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(encodedPkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(encodedPkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("Ed448 private key specification has been destroyed"); + } } } diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PublicKeyBuilder.java index a48cd47..bf6566f 100644 --- a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PublicKeyBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PublicKeyBuilder.java @@ -35,7 +35,6 @@ package zeroecho.core.alg.ed448; import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder; import zeroecho.core.spec.AlgorithmKeySpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; /** *

      Ed448 Public Key Builder

      @@ -65,8 +64,8 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * }
      * *

      Thread-safety

      Stateless and safe for concurrent use. Each call to - * {@link AsymmetricKeyBuilder#importPublic(AlgorithmKeySpec) - * importPublic(Ed448PublicKeySpec)} creates a new + * {@link zeroecho.core.spi.PublicKeyImporter#importPublic(AlgorithmKeySpec)} + * creates a new * {@link java.security.KeyFactory}. * * @since 1.0 diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/package-info.java b/lib/src/main/java/zeroecho/core/alg/ed448/package-info.java index c7b3712..3d6c8ac 100644 --- a/lib/src/main/java/zeroecho/core/alg/ed448/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/ed448/package-info.java @@ -38,7 +38,8 @@ * This package wires the Ed448 Edwards-curve Digital Signature Algorithm into * the core layer. It provides the algorithm descriptor, a streaming signature * context with a fixed 114-byte tag length, builders for generating and - * importing keys, and immutable key specifications with marshalling helpers. + * importing keys, and defensively copying key specifications with marshalling + * helpers. *

      * *

      Scope and responsibilities

      @@ -49,8 +50,8 @@ * enforces the 114-byte tag size. *
    38. Expose builders for key-pair generation and for importing encoded * keys.
    39. - *
    40. Define immutable key specifications suitable for safe cloning and simple - * marshalling.
    41. + *
    42. Define defensively copying key specifications suitable for safe cloning + * and simple marshalling; private-key specifications are destroyable.
    43. * * *

      Components

      @@ -63,9 +64,9 @@ * marker spec for producing key pairs. *
    44. Ed448PublicKeyBuilder / Ed448PrivateKeyBuilder: importers * backed by JCA key factories.
    45. - *
    46. Ed448PublicKeySpec / Ed448PrivateKeySpec: immutable - * wrappers over X.509 and PKCS#8 encodings, with defensive copying and base64 - * marshalling helpers.
    47. + *
    48. Ed448PublicKeySpec / Ed448PrivateKeySpec: wrappers over + * X.509 and PKCS#8 encodings, with defensive copying and base64 marshalling + * helpers; the private-key form is destroyable.
    49. * * *

      Design notes

      diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalAlgorithm.java index d6dfb65..9db8243 100644 --- a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalAlgorithm.java @@ -47,6 +47,7 @@ import java.security.SecureRandom; import java.security.Security; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; import org.bouncycastle.jce.spec.ElGamalParameterSpec; @@ -54,7 +55,9 @@ import zeroecho.core.AlgorithmFamily; import zeroecho.core.KeyUsage; import zeroecho.core.alg.AbstractCryptoAlgorithm; import zeroecho.core.context.EncryptionContext; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; /** *

      ElGamal Asymmetric Encryption Algorithm

      @@ -110,11 +113,12 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * *

      Example

      {@code
        * CryptoAlgorithm algo = new ElgamalAlgorithm();
      - * KeyPair kp = algo.generateKeyPair(ElgamalParamSpec.ffdhe2048());
      + * KeyPair kp = algo.asymmetricKeyPairGenerator(ElgamalParamSpec.class)
      + *                  .generateKeyPair(ElgamalParamSpec.ffdhe2048());
        *
      - * EncryptionContext enc = algo.create(KeyUsage.ENCRYPT, kp.getPublic(),
      + * EncryptionContext enc = algo.createContext(KeyUsage.ENCRYPT, kp.getPublic(),
        *                                     ElgamalEncSpec.pkcs1());
      - * EncryptionContext dec = algo.create(KeyUsage.DECRYPT, kp.getPrivate(),
      + * EncryptionContext dec = algo.createContext(KeyUsage.DECRYPT, kp.getPrivate(),
        *                                     ElgamalEncSpec.pkcs1());
        * }
      * @@ -148,7 +152,7 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm { if (false) { // NOPMD // this key generation is slow - registerAsymmetricKeyBuilder(ElgamalKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + registerAsymmetricKeyPairGenerator(ElgamalKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() { @Override public KeyPair generateKeyPair(ElgamalKeyGenSpec spec) throws GeneralSecurityException { ensureBC(); @@ -161,20 +165,10 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm { kpg.initialize(eg, new SecureRandom()); return kpg.generateKeyPair(); } - - @Override - public PublicKey importPublic(ElgamalKeyGenSpec spec) { - throw new UnsupportedOperationException("Use ElgamalPublicKeySpec to import a public key."); - } - - @Override - public PrivateKey importPrivate(ElgamalKeyGenSpec spec) { - throw new UnsupportedOperationException("Use ElgamalPrivateKeySpec to import a private key."); - } }, ElgamalKeyGenSpec::elgamal2048); } - registerAsymmetricKeyBuilder(ElgamalParamSpec.class, new AsymmetricKeyBuilder<>() { + registerAsymmetricKeyPairGenerator(ElgamalParamSpec.class, new AsymmetricKeyPairGenerator<>() { @Override public KeyPair generateKeyPair(ElgamalParamSpec spec) throws GeneralSecurityException { ensureBC(); @@ -183,23 +177,9 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm { kpg.initialize(eg, new SecureRandom()); return kpg.generateKeyPair(); } - - @Override - public PublicKey importPublic(ElgamalParamSpec spec) { - throw new UnsupportedOperationException("Use ElgamalPublicKeySpec to import a public key."); - } - - @Override - public PrivateKey importPrivate(ElgamalParamSpec spec) { - throw new UnsupportedOperationException("Use ElgamalPrivateKeySpec to import a private key."); - } }, ElgamalParamSpec::ffdhe2048); - registerAsymmetricKeyBuilder(ElgamalPublicKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(ElgamalPublicKeySpec spec) { - throw new UnsupportedOperationException("Generation not supported for encoded spec."); - } + registerPublicKeyImporter(ElgamalPublicKeySpec.class, new PublicKeyImporter<>() { @Override public PublicKey importPublic(ElgamalPublicKeySpec spec) throws GeneralSecurityException { @@ -207,31 +187,22 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm { KeyFactory kf = KeyFactory.getInstance(EL_GAMAL, providerName()); return kf.generatePublic(new X509EncodedKeySpec(spec.encoded())); } + }); - @Override - public PrivateKey importPrivate(ElgamalPublicKeySpec spec) { - throw new UnsupportedOperationException("Use ElgamalPrivateKeySpec for private keys."); - } - }, null); - - registerAsymmetricKeyBuilder(ElgamalPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(ElgamalPrivateKeySpec spec) { - throw new UnsupportedOperationException("Generation not supported for encoded spec."); - } - - @Override - public PublicKey importPublic(ElgamalPrivateKeySpec spec) { - throw new UnsupportedOperationException("Use ElgamalPublicKeySpec for public keys."); - } + registerPrivateKeyImporter(ElgamalPrivateKeySpec.class, new PrivateKeyImporter<>() { @Override public PrivateKey importPrivate(ElgamalPrivateKeySpec spec) throws GeneralSecurityException { ensureBC(); KeyFactory kf = KeyFactory.getInstance(EL_GAMAL, providerName()); - return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); + byte[] encoded = spec.encoded(); + try { + return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } finally { + Arrays.fill(encoded, (byte) 0); + } } - }, null); + }); } /** diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalCipherContext.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalCipherContext.java index 3a2b7e6..7b5da5b 100644 --- a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalCipherContext.java +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalCipherContext.java @@ -137,7 +137,7 @@ public final class ElgamalCipherContext implements EncryptionContext { return CipherTransformInputStreamBuilder.builder().withCipher(cipher).withUpstream(upstream) .withInputBlockSize(g.inputBlockSize()).withOutputBlockSize(g.perBlockOutput()) - .withLeftZeroPadding(g.noPadding).build(); + .withLeftZeroPadding(g.noPadding).withIndependentBlocks().build(); } /** diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalEncSpec.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalEncSpec.java index 93ce86e..89a69b3 100644 --- a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalEncSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalEncSpec.java @@ -71,7 +71,7 @@ import zeroecho.core.spec.ContextSpec; * *

      Usage

      Instances are created via the static factories:
      {@code
        * ElgamalEncSpec spec = ElgamalEncSpec.pkcs1();
      - * EncryptionContext ctx = algo.create(KeyUsage.ENCRYPT, pubKey, spec);
      + * EncryptionContext ctx = algo.createContext(KeyUsage.ENCRYPT, pubKey, spec);
        * }
      * *

      Thread-safety

      {@code ElgamalEncSpec} is immutable and safe to share diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalKeyGenSpec.java index d376aea..d95e5aa 100644 --- a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalKeyGenSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalKeyGenSpec.java @@ -64,7 +64,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * *

      Usage

      {@code
        * ElgamalKeyGenSpec spec = ElgamalKeyGenSpec.elgamal2048();
      - * KeyPair kp = algo.generateKeyPair(spec);
      + * KeyPair kp = algo.asymmetricKeyPairGenerator(ElgamalKeyGenSpec.class).generateKeyPair(spec);
        * }
      * *

      Thread-safety

      Instances are immutable and can be freely shared diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalParamSpec.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalParamSpec.java index 0f1ac44..465a789 100644 --- a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalParamSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalParamSpec.java @@ -73,7 +73,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * *

      Usage

      {@code
        * ElgamalParamSpec spec = ElgamalParamSpec.ffdhe2048();
      - * KeyPair kp = algo.generateKeyPair(spec);
      + * KeyPair kp = algo.asymmetricKeyPairGenerator(ElgamalParamSpec.class).generateKeyPair(spec);
        * }
      * *

      Thread-safety

      Instances are immutable and safe to share between diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalPrivateKeySpec.java index aaf1693..470a389 100644 --- a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalPrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalPrivateKeySpec.java @@ -33,7 +33,11 @@ ******************************************************************************/ package zeroecho.core.alg.elgamal; +import java.util.Arrays; import java.util.Base64; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.spec.AlgorithmKeySpec; @@ -62,7 +66,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * // Import from PKCS#8 DER * byte[] der = Files.readAllBytes(Path.of("elgamal-priv.der")); * ElgamalPrivateKeySpec spec = new ElgamalPrivateKeySpec(der); - * PrivateKey priv = algo.importPrivate(spec); + * PrivateKey priv = algo.privateKeyImporter(ElgamalPrivateKeySpec.class).importPrivate(spec); * * // Marshal for serialization * PairSeq ps = ElgamalPrivateKeySpec.marshal(spec); @@ -79,15 +83,16 @@ import zeroecho.core.spec.AlgorithmKeySpec; * handling when possible. * * - *

      Thread-safety

      Instances are immutable and safe to share between - * threads. + *

      Thread-safety

      Access and destruction are synchronized. * * @since 1.0 */ -public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec { +public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] pkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Constructs a new private key spec from PKCS#8-encoded bytes. @@ -104,7 +109,13 @@ public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec { * @return PKCS#8 DER encoding */ public byte[] encoded() { - return pkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return pkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -122,7 +133,7 @@ public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec { * @return marshalled key as {@link PairSeq} */ public static PairSeq marshal(ElgamalPrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "ELGAMAL-PRIV", PKCS8_B64, b64); } @@ -141,12 +152,62 @@ public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec { String k = cur.key(); String v = cur.value(); if (PKCS8_B64.equals(k)) { - out = Base64.getDecoder().decode(v); + out = decodeReplacing(out, v); } } if (out == null) { throw new IllegalArgumentException("pkcs8.b64 missing for ElGamal private key"); } - return new ElgamalPrivateKeySpec(out); + try { + return new ElgamalPrivateKeySpec(out); + } finally { + Arrays.fill(out, (byte) 0); + } + } + + private static byte[] decodeReplacing(byte[] current, String encoded) { + if (current != null) { + Arrays.fill(current, (byte) 0); + } + return Base64.getDecoder().decode(encoded); + } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(pkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(pkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("ElGamal private key specification has been destroyed"); + } } } diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalPublicKeySpec.java index a7e35bb..66576d3 100644 --- a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalPublicKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalPublicKeySpec.java @@ -62,7 +62,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * // Import from X.509 DER * byte[] der = Files.readAllBytes(Path.of("elgamal-pub.der")); * ElgamalPublicKeySpec spec = new ElgamalPublicKeySpec(der); - * PublicKey pub = algo.importPublic(spec); + * PublicKey pub = algo.publicKeyImporter(ElgamalPublicKeySpec.class).importPublic(spec); * * // Marshal for serialization * PairSeq ps = ElgamalPublicKeySpec.marshal(spec); diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/package-info.java b/lib/src/main/java/zeroecho/core/alg/elgamal/package-info.java index 5af44ac..ccc3ebe 100644 --- a/lib/src/main/java/zeroecho/core/alg/elgamal/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/package-info.java @@ -70,9 +70,9 @@ *
    50. ElgamalKeyGenSpec: parameters for generating fresh domain * parameters and key pairs; typically disabled in favor of predefined parameter * sets.
    51. - *
    52. ElgamalPublicKeySpec / ElgamalPrivateKeySpec: immutable - * encoded key specifications (X.509 and PKCS#8) with defensive copying and - * compact marshalling helpers.
    53. + *
    54. ElgamalPublicKeySpec / ElgamalPrivateKeySpec: encoded key + * specifications (X.509 and PKCS#8) with defensive copying and compact + * marshalling helpers; the private-key form is destroyable.
    55. * * *

      Design notes

      diff --git a/lib/src/main/java/zeroecho/core/alg/frodo/FrodoAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoAlgorithm.java index 26d667e..d3c7b1f 100644 --- a/lib/src/main/java/zeroecho/core/alg/frodo/FrodoAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoAlgorithm.java @@ -44,6 +44,7 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; import java.security.spec.X509EncodedKeySpec; import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; @@ -56,7 +57,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; import zeroecho.core.context.KemContext; import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.spec.VoidSpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; /** *

      Frodo Key Encapsulation Mechanism (KEM)

      @@ -104,9 +107,8 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * X.509 encoding. *
    56. Private keys may be imported from {@link FrodoPrivateKeySpec} using a * PKCS#8 encoding.
    57. - *
    58. Direct import of key specs via {@code generateKeyPair} in the spec-based - * builders is not supported and will throw - * {@link UnsupportedOperationException}.
    59. + *
    60. Generation and public/private import are registered as independent exact + * capabilities.
    61. * * *

      Provider requirements

      @@ -120,13 +122,14 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; *

      Example usage

      {@code
        * // Generate a Frodo keypair
        * CryptoAlgorithm frodo = CryptoAlgorithms.require("Frodo");
      - * KeyPair kp = frodo.generateKeyPair(FrodoKeyGenSpec.frodo1344aes());
      + * KeyPair kp = frodo.asymmetricKeyPairGenerator(FrodoKeyGenSpec.class)
      + *                   .generateKeyPair(FrodoKeyGenSpec.frodo1344aes());
        *
        * // Encapsulate using the recipient's public key
      - * KemContext enc = frodo.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
      + * KemContext enc = frodo.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
        * 
        * // Decapsulate using the recipient's private key
      - * KemContext dec = frodo.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
      + * KemContext dec = frodo.createContext(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
        * }
      * *

      Thread-safety

      @@ -201,7 +204,7 @@ public final class FrodoAlgorithm extends AbstractCryptoAlgorithm { .build(); }, () -> VoidSpec.INSTANCE); - registerAsymmetricKeyBuilder(FrodoKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + registerAsymmetricKeyPairGenerator(FrodoKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() { @Override public KeyPair generateKeyPair(FrodoKeyGenSpec spec) throws GeneralSecurityException { ensureProvider(); @@ -217,23 +220,9 @@ public final class FrodoAlgorithm extends AbstractCryptoAlgorithm { kpg.initialize(params, new SecureRandom()); return kpg.generateKeyPair(); } - - @Override - public PublicKey importPublic(FrodoKeyGenSpec spec) { - throw new UnsupportedOperationException(); - } - - @Override - public PrivateKey importPrivate(FrodoKeyGenSpec spec) { - throw new UnsupportedOperationException(); - } }, FrodoKeyGenSpec::frodo1344aes); - registerAsymmetricKeyBuilder(FrodoPublicKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(FrodoPublicKeySpec spec) { - throw new UnsupportedOperationException(); - } + registerPublicKeyImporter(FrodoPublicKeySpec.class, new PublicKeyImporter<>() { @Override public PublicKey importPublic(FrodoPublicKeySpec spec) throws GeneralSecurityException { @@ -241,31 +230,22 @@ public final class FrodoAlgorithm extends AbstractCryptoAlgorithm { KeyFactory kf = KeyFactory.getInstance("Frodo", providerName()); return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); } + }); - @Override - public PrivateKey importPrivate(FrodoPublicKeySpec spec) { - throw new UnsupportedOperationException(); - } - }, null); - - registerAsymmetricKeyBuilder(FrodoPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(FrodoPrivateKeySpec spec) { - throw new UnsupportedOperationException(); - } - - @Override - public PublicKey importPublic(FrodoPrivateKeySpec spec) { - throw new UnsupportedOperationException(); - } + registerPrivateKeyImporter(FrodoPrivateKeySpec.class, new PrivateKeyImporter<>() { @Override public PrivateKey importPrivate(FrodoPrivateKeySpec spec) throws GeneralSecurityException { ensureProvider(); KeyFactory kf = KeyFactory.getInstance("Frodo", providerName()); - return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + byte[] encoded = spec.pkcs8(); + try { + return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } finally { + Arrays.fill(encoded, (byte) 0); + } } - }, null); + }); } private static void ensureProvider() throws NoSuchProviderException { diff --git a/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPrivateKeySpec.java index c7621b1..c3f1a41 100644 --- a/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPrivateKeySpec.java @@ -33,10 +33,13 @@ ******************************************************************************/ package zeroecho.core.alg.frodo; +import java.util.Arrays; import java.util.Base64; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; -import zeroecho.core.CryptoAlgorithm; import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq.Cursor; import zeroecho.core.spec.AlgorithmKeySpec; @@ -45,7 +48,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; *

      Specification for importing a Frodo private key

      * * {@code FrodoPrivateKeySpec} is a simple wrapper around a PKCS#8-encoded - * FrodoKEM private key. It provides immutable access to the raw encoding and + * FrodoKEM private key. It provides defensive access to the raw encoding and * utilities for serialization. * *

      Encoding format

      @@ -59,9 +62,8 @@ import zeroecho.core.spec.AlgorithmKeySpec; *

      Usage

      *
        *
      • Instances of this spec can be passed to - * {@link CryptoAlgorithm#importPrivate(AlgorithmKeySpec) - * importPrivate(FrodoPrivateKeySpec)} to construct a usable - * {@link java.security.PrivateKey} object.
      • + * {@link zeroecho.sdk.KeyBuilders.Asymmetric#importPrivate(String, AlgorithmKeySpec)} + * to construct a usable {@link java.security.PrivateKey} object. *
      • The {@link #marshal(FrodoPrivateKeySpec)} and {@link #unmarshal(PairSeq)} * helpers allow safe conversion to/from structured textual form for persistence * or transmission.
      • @@ -71,21 +73,23 @@ import zeroecho.core.spec.AlgorithmKeySpec; * // Import an existing Frodo private key * byte[] encoded = Files.readAllBytes(Paths.get("frodo.key")); * FrodoPrivateKeySpec spec = new FrodoPrivateKeySpec(encoded); - * PrivateKey priv = frodo.importPrivate(spec); + * PrivateKey priv = session.keyBuilders().asymmetric().importPrivate("FrodoKEM", spec); * }
      * *

      Thread-safety

      *

      - * This class is immutable; the internal byte array is cloned on construction - * and when accessed via {@link #pkcs8()}. + * The internal byte array is cloned on construction and when accessed via + * {@link #pkcs8()}. Access and destruction are synchronized. *

      * * @since 1.0 */ -public final class FrodoPrivateKeySpec implements AlgorithmKeySpec { +public final class FrodoPrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] pkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Creates a new specification from a PKCS#8 DER-encoded key. @@ -103,7 +107,13 @@ public final class FrodoPrivateKeySpec implements AlgorithmKeySpec { * @return defensive copy of the PKCS#8-encoded private key */ public byte[] pkcs8() { - return pkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return pkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -114,7 +124,7 @@ public final class FrodoPrivateKeySpec implements AlgorithmKeySpec { * @return a {@code PairSeq} with type and Base64-encoded key */ public static PairSeq marshal(FrodoPrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "FrodoPrivateKeySpec", PKCS8_B64, b64); } @@ -136,7 +146,12 @@ public final class FrodoPrivateKeySpec implements AlgorithmKeySpec { if (b64 == null) { throw new IllegalArgumentException("FrodoPrivateKeySpec: missing pkcs8.b64"); } - return new FrodoPrivateKeySpec(Base64.getDecoder().decode(b64)); + byte[] decoded = Base64.getDecoder().decode(b64); + try { + return new FrodoPrivateKeySpec(decoded); + } finally { + Arrays.fill(decoded, (byte) 0); + } } /** @@ -148,4 +163,43 @@ public final class FrodoPrivateKeySpec implements AlgorithmKeySpec { public String toString() { return "FrodoPrivateKeySpec[len=" + pkcs8.length + "]"; } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(pkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(pkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("Frodo private key specification has been destroyed"); + } + } } diff --git a/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPublicKeySpec.java index afb6d4d..5609ced 100644 --- a/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPublicKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPublicKeySpec.java @@ -36,7 +36,6 @@ package zeroecho.core.alg.frodo; import java.util.Base64; import java.util.Objects; -import zeroecho.core.CryptoAlgorithm; import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq.Cursor; import zeroecho.core.spec.AlgorithmKeySpec; @@ -59,9 +58,8 @@ import zeroecho.core.spec.AlgorithmKeySpec; *

      Usage

      *
        *
      • Instances of this spec can be passed to - * {@link CryptoAlgorithm#importPublic(AlgorithmKeySpec) - * importPublic(FrodoPublicKeySpec)} to construct a usable - * {@link java.security.PublicKey}.
      • + * {@link zeroecho.sdk.KeyBuilders.Asymmetric#importPublic(String, AlgorithmKeySpec)} + * to construct a usable {@link java.security.PublicKey}. *
      • The {@link #marshal(FrodoPublicKeySpec)} and {@link #unmarshal(PairSeq)} * methods allow safe serialization into and recovery from structured textual * form.
      • @@ -71,7 +69,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * // Import an existing Frodo public key * byte[] encoded = Files.readAllBytes(Paths.get("frodo.pub")); * FrodoPublicKeySpec spec = new FrodoPublicKeySpec(encoded); - * PublicKey pub = frodo.importPublic(spec); + * PublicKey pub = session.keyBuilders().asymmetric().importPublic("FrodoKEM", spec); * } * *

        Thread-safety

        diff --git a/lib/src/main/java/zeroecho/core/alg/frodo/package-info.java b/lib/src/main/java/zeroecho/core/alg/frodo/package-info.java index af34e26..20efe16 100644 --- a/lib/src/main/java/zeroecho/core/alg/frodo/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/frodo/package-info.java @@ -51,8 +51,9 @@ * role for initiator/responder workflows. *
      • Provide a {@link zeroecho.core.context.KemContext} implementation bound * to either a public or private key for encapsulation or decapsulation.
      • - *
      • Expose immutable specifications for key generation variants and encoded - * key carriers with marshalling helpers.
      • + *
      • Expose immutable key-generation specifications and defensively copying + * encoded-key carriers with marshalling helpers; private-key carriers are + * destroyable.
      • *
      • Ensure operations are delegated to a supported PQC provider (BouncyCastle * PQC) and fail fast if absent.
      • *
      diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/HmacAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacAlgorithm.java index bda8c17..8e3eee1 100644 --- a/lib/src/main/java/zeroecho/core/alg/hmac/HmacAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacAlgorithm.java @@ -33,9 +33,9 @@ ******************************************************************************/ package zeroecho.core.alg.hmac; -import java.io.IOException; import java.security.GeneralSecurityException; import java.security.SecureRandom; +import java.util.Arrays; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; @@ -45,7 +45,9 @@ import zeroecho.core.AlgorithmFamily; import zeroecho.core.KeyUsage; import zeroecho.core.alg.AbstractCryptoAlgorithm; import zeroecho.core.context.MacContext; -import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.core.err.ProviderFailureException; +import zeroecho.core.spi.SymmetricKeyGenerator; +import zeroecho.core.spi.SymmetricKeyImporter; /** *

      HMAC Algorithm Integration

      @@ -86,11 +88,11 @@ import zeroecho.core.spi.SymmetricKeyBuilder; * *

      Usage example

      {@code
        * // Generate a fresh key for HMAC-SHA256
      - * SecretKey key = CryptoAlgorithms.generateSecret("HMAC",
      + * SecretKey key = session.keyBuilders().symmetric().generate("HMAC",
        *     HmacKeyGenSpec.sha256(256));
        *
        * // Create a MAC context
      - * MacContext ctx = CryptoAlgorithms.create("HMAC",
      + * MacContext ctx = session.createContext("HMAC",
        *     KeyUsage.MAC, key, HmacSpec.sha256());
        *
        * ctx.update(data);
      @@ -133,37 +135,33 @@ public final class HmacAlgorithm extends AbstractCryptoAlgorithm {
                           try {
                               return new HmacMacContext(this, k, s.macName());
                           } catch (GeneralSecurityException e) {
      -                        throw new IOException("Init HMAC failed for " + s.macName(), e);
      +                        throw new ProviderFailureException("Failed to initialize HMAC " + s.macName(), e);
                           }
                       }, HmacSpec::sha256 // default for catalog/tests
               );
       
               // Key builders (generation/import) — both respect macName in the spec.
      -        registerSymmetricKeyBuilder(HmacKeyGenSpec.class, new SymmetricKeyBuilder<>() {
      +        registerSymmetricKeyGenerator(HmacKeyGenSpec.class, new SymmetricKeyGenerator<>() {
                   @Override
                   public SecretKey generateSecret(HmacKeyGenSpec spec) throws GeneralSecurityException {
                       KeyGenerator kg = KeyGenerator.getInstance(spec.macName());
                       kg.init(spec.keySizeBits(), new SecureRandom());
                       return kg.generateKey();
                   }
      -
      -            @Override
      -            public SecretKey importSecret(HmacKeyGenSpec spec) {
      -                throw new UnsupportedOperationException("Use HmacKeyImportSpec for import");
      -            }
               }, () -> HmacKeyGenSpec.sha256(256) // default keygen spec
               );
       
      -        registerSymmetricKeyBuilder(HmacKeyImportSpec.class, new SymmetricKeyBuilder<>() {
      -            @Override
      -            public SecretKey generateSecret(HmacKeyImportSpec spec) {
      -                throw new UnsupportedOperationException("Use HmacKeyGenSpec for generation");
      -            }
      +        registerSymmetricKeyImporter(HmacKeyImportSpec.class, new SymmetricKeyImporter<>() {
       
                   @Override
                   public SecretKey importSecret(HmacKeyImportSpec spec) {
      -                return new SecretKeySpec(spec.key(), spec.macName());
      +                byte[] key = spec.key();
      +                try {
      +                    return new SecretKeySpec(key, spec.macName());
      +                } finally {
      +                    Arrays.fill(key, (byte) 0);
      +                }
                   }
      -        }, null);
      +        });
           }
       }
      diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyGenSpec.java
      index 56167e2..cd3df14 100644
      --- a/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyGenSpec.java
      +++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyGenSpec.java
      @@ -55,12 +55,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
        *
        * 

      Usage

      Typical usage is to construct a spec with a given digest * family and key size, and then pass it to a registered - * {@code SymmetricKeyBuilder}: + * {@link zeroecho.core.spi.SymmetricKeyGenerator}: * *
      {@code
        * // Generate a 256-bit key for HMAC-SHA256
        * HmacKeyGenSpec spec = HmacKeyGenSpec.sha256(256);
      - * SecretKey key = CryptoAlgorithms.generateSecret("HMAC", spec);
      + * SecretKey key = session.keyBuilders().symmetric().generate("HMAC", spec);
        * }
      * *

      Defaults

      Convenience static factories are provided for the most diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyImportSpec.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyImportSpec.java index fe9087e..98efe41 100644 --- a/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyImportSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyImportSpec.java @@ -34,8 +34,12 @@ package zeroecho.core.alg.hmac; import java.util.Base64; +import java.util.Arrays; import java.util.HexFormat; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.annotation.Describable; import zeroecho.core.marshal.PairSeq; @@ -66,7 +70,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * {@code * byte[] rawKey = Files.readAllBytes(Paths.get("hmac.key")); * HmacKeyImportSpec spec = HmacKeyImportSpec.fromRaw("HmacSHA256", rawKey); - * SecretKey key = CryptoAlgorithms.importSecret("HMAC", spec); + * SecretKey key = session.keyBuilders().symmetric().importKey("HMAC", spec); * } *
      * @@ -100,9 +104,11 @@ import zeroecho.core.spec.AlgorithmKeySpec; * * @since 1.0 */ -public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable { +public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable, Destroyable { private final String macName; private final byte[] key; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Constructs a new HMAC key import specification. @@ -132,7 +138,13 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable { * @return cloned key bytes */ public byte[] key() { - return key.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return key.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -156,7 +168,12 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable { */ public static HmacKeyImportSpec fromHex(String macName, String hex) { Objects.requireNonNull(hex, "hex must not be null"); - return fromRaw(macName, HexFormat.of().parseHex(hex)); + byte[] decoded = HexFormat.of().parseHex(hex); + try { + return fromRaw(macName, decoded); + } finally { + Arrays.fill(decoded, (byte) 0); + } } /** @@ -169,7 +186,12 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable { */ public static HmacKeyImportSpec fromBase64(String macName, String b64) { Objects.requireNonNull(b64, "base64 must not be null"); - return fromRaw(macName, Base64.getDecoder().decode(b64)); + byte[] decoded = Base64.getDecoder().decode(b64); + try { + return fromRaw(macName, decoded); + } finally { + Arrays.fill(decoded, (byte) 0); + } } /** @@ -193,7 +215,7 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable { * @return encoded key spec sequence */ public static PairSeq marshal(HmacKeyImportSpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.key); + String b64 = spec.encodedKey(); return PairSeq.of("type", "HMAC-KEY", "mac", spec.macName, "k.b64", b64); } @@ -213,24 +235,81 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable { String mac = null; byte[] key = null; - PairSeq.Cursor cur = p.cursor(); - while (cur.next()) { - String k = cur.key(); - String v = cur.value(); - switch (k) { - case "mac" -> mac = v; - case "k.b64" -> key = Base64.getDecoder().decode(v); - case "k.hex" -> key = HexFormat.of().parseHex(v); - default -> { + try { + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + switch (k) { + case "mac" -> mac = v; + case "k.b64" -> { + wipe(key); + key = Base64.getDecoder().decode(v); + } + case "k.hex" -> { + wipe(key); + key = HexFormat.of().parseHex(v); + } + default -> { + } } } + if (mac == null) { + throw new IllegalArgumentException("mac missing for HMAC key"); + } + if (key == null) { + throw new IllegalArgumentException("HMAC key missing (k.b64 or k.hex)"); + } + return new HmacKeyImportSpec(mac, key); + } finally { + wipe(key); } - if (mac == null) { - throw new IllegalArgumentException("mac missing for HMAC key"); + } + + private static void wipe(byte[] current) { + if (current != null) { + Arrays.fill(current, (byte) 0); } - if (key == null) { - throw new IllegalArgumentException("HMAC key missing (k.b64 or k.hex)"); + } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(key); + } finally { + lifecycleLock.unlock(); + } + } + + /** {@inheritDoc} */ + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(key, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + /** {@inheritDoc} */ + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("HMAC key import specification has been destroyed"); } - return new HmacKeyImportSpec(mac, key); } } diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/HmacMacContext.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacMacContext.java index 42a8c3e..9d81f96 100644 --- a/lib/src/main/java/zeroecho/core/alg/hmac/HmacMacContext.java +++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacMacContext.java @@ -100,7 +100,7 @@ import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate; *

      Usage example

      *

      Produce HMAC-SHA256 trailer

        * {@code
      - * SecretKey key = CryptoAlgorithms.generateSecret("HMAC", HmacKeyGenSpec.sha256(256));
      + * SecretKey key = session.keyBuilders().symmetric().generate("HMAC", HmacKeyGenSpec.sha256(256));
        * HmacMacContext ctx = new HmacMacContext(CryptoAlgorithms.require("HMAC"), key, "HmacSHA256");
        * try (InputStream in = ctx.wrap(new FileInputStream("data.bin"))) {
        *     in.transferTo(OutputStream.nullOutputStream()); // body then MAC trailer
      @@ -135,7 +135,7 @@ public final class HmacMacContext implements MacContext {
       
           // lifecycle
           private boolean wrapped; // = false;
      -    private Stream activeStream;
      +    private HmacStream activeStream;
           private boolean autoCloseActiveStream;
       
           /**
      @@ -238,7 +238,7 @@ public final class HmacMacContext implements MacContext {
                   throw new IOException("HMAC init failed for " + macName, e);
               }
       
      -        Stream s = new Stream(upstream, mac, macName, expectedTag, verifier());
      +        HmacStream s = new HmacStream(upstream, mac, macName, expectedTag, verifier());
               this.activeStream = s;
               return s;
           }
      diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/HmacSpec.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacSpec.java
      index 4cc324a..943f73a 100644
      --- a/lib/src/main/java/zeroecho/core/alg/hmac/HmacSpec.java
      +++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacSpec.java
      @@ -54,10 +54,10 @@ import zeroecho.core.spec.ContextSpec;
        *
        * 

      Usage

      This spec is passed when creating a new HMAC context: *
      {@code
      - * SecretKey key = CryptoAlgorithms.generateSecret("HMAC",
      + * SecretKey key = session.keyBuilders().symmetric().generate("HMAC",
        *     HmacKeyGenSpec.sha256(256));
        *
      - * MacContext ctx = CryptoAlgorithms.create("HMAC",
      + * MacContext ctx = session.createContext("HMAC",
        *     KeyUsage.MAC, key, HmacSpec.sha256());
        *
        * ctx.update(data);
      diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/Stream.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacStream.java
      similarity index 95%
      rename from lib/src/main/java/zeroecho/core/alg/hmac/Stream.java
      rename to lib/src/main/java/zeroecho/core/alg/hmac/HmacStream.java
      index 2867b61..44c4101 100644
      --- a/lib/src/main/java/zeroecho/core/alg/hmac/Stream.java
      +++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacStream.java
      @@ -81,7 +81,7 @@ import zeroecho.core.util.Strings;
        * Mac mac = Mac.getInstance("HmacSHA256");
        * mac.init(secretKey);
        * try (InputStream in = Files.newInputStream(path);
      - *      InputStream s  = new Stream(in, mac, "HmacSHA256", null,
      + *      InputStream s  = new HmacStream(in, mac, "HmacSHA256", null,
        *                                  new ByteVerificationStrategy())) {
        *     // read from 's' to consume body; trailer is produced automatically
        * }
      @@ -98,14 +98,14 @@ import zeroecho.core.util.Strings;
        *     new ByteVerificationStrategy().getThrowOnMismatch();
        *
        * try (InputStream in = Files.newInputStream(path);
      - *      InputStream s  = new Stream(in, macV, "HmacSHA256", expected, strategy)) {
      + *      InputStream s  = new HmacStream(in, macV, "HmacSHA256", expected, strategy)) {
        *     // read from 's'; exception is thrown at EOF if verification fails
        * }
        * }
        * 
      */ -final class Stream extends AbstractPassthroughInputStream { - private static final Logger LOG = Logger.getLogger(Stream.class.getName()); +final class HmacStream extends AbstractPassthroughInputStream { + private static final Logger LOG = Logger.getLogger(HmacStream.class.getName()); private final Mac mac; private final String macName; @@ -135,7 +135,7 @@ final class Stream extends AbstractPassthroughInputStream { * @throws NullPointerException if {@code upstream}, {@code mac}, or * {@code macName} is {@code null} */ - /* package */ Stream(final InputStream upstream, final Mac mac, final String macName, final byte[] expectedTag, + /* package */ HmacStream(final InputStream upstream, final Mac mac, final String macName, final byte[] expectedTag, final VerificationBiPredicate verificationStrategy) { super(upstream, 8192); this.mac = mac; diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/package-info.java b/lib/src/main/java/zeroecho/core/alg/hmac/package-info.java index 74eed74..eb2d680 100644 --- a/lib/src/main/java/zeroecho/core/alg/hmac/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/hmac/package-info.java @@ -48,8 +48,8 @@ *
    62. Expose a streaming {@link zeroecho.core.context.MacContext} that appends * tags in produce mode or verifies an expected tag at end of stream in verify * mode.
    63. - *
    64. Provide immutable specs for selecting the HMAC variant and for supplying - * keys (generation or import of raw key material).
    65. + *
    66. Provide immutable specs for selecting the HMAC variant and destroyable + * specs for importing raw key material.
    67. *
    68. Encapsulate JCA/JCE interop and provider checks behind small * factories.
    69. * @@ -68,7 +68,7 @@ * specific HMAC variant. *
    70. HmacKeyImportSpec: wrapper for importing existing raw keys, with * Base64/hex helpers.
    71. - *
    72. Stream: internal passthrough input stream implementing the + *
    73. HmacStream: internal passthrough input stream implementing the * byte-pumping and trailer/verification logic for the MAC context.
    74. * * diff --git a/lib/src/main/java/zeroecho/core/alg/hqc/HqcAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/hqc/HqcAlgorithm.java index 329f3f7..30632ae 100644 --- a/lib/src/main/java/zeroecho/core/alg/hqc/HqcAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/hqc/HqcAlgorithm.java @@ -44,6 +44,7 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; import java.security.spec.X509EncodedKeySpec; import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; @@ -56,7 +57,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; import zeroecho.core.context.KemContext; import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.spec.VoidSpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; /** *

      HQC (Hamming Quasi-Cyclic) Algorithm Integration

      @@ -124,13 +127,14 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; *

      Usage example

      {@code
        * // Generate an HQC key pair
        * HqcAlgorithm hqc = new HqcAlgorithm();
      - * KeyPair kp = hqc.generateKeyPair(HqcKeyGenSpec.hqc256());
      + * KeyPair kp = hqc.asymmetricKeyPairGenerator(HqcKeyGenSpec.class)
      + *                  .generateKeyPair(HqcKeyGenSpec.hqc256());
        *
        * // Encapsulation by initiator
      - * KemContext encapsCtx = hqc.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
      + * KemContext encapsCtx = hqc.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
        *
        * // Decapsulation by responder
      - * KemContext decapsCtx = hqc.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
      + * KemContext decapsCtx = hqc.createContext(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
        * }
      * * @since 1.0 @@ -200,7 +204,7 @@ public final class HqcAlgorithm extends AbstractCryptoAlgorithm { .build(); }, () -> VoidSpec.INSTANCE); - registerAsymmetricKeyBuilder(HqcKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + registerAsymmetricKeyPairGenerator(HqcKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() { @Override public KeyPair generateKeyPair(HqcKeyGenSpec spec) throws GeneralSecurityException { ensureProvider(); @@ -213,23 +217,9 @@ public final class HqcAlgorithm extends AbstractCryptoAlgorithm { kpg.initialize(params, new SecureRandom()); return kpg.generateKeyPair(); } - - @Override - public PublicKey importPublic(HqcKeyGenSpec spec) { - throw new UnsupportedOperationException(); - } - - @Override - public PrivateKey importPrivate(HqcKeyGenSpec spec) { - throw new UnsupportedOperationException(); - } }, HqcKeyGenSpec::hqc256); - registerAsymmetricKeyBuilder(HqcPublicKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(HqcPublicKeySpec spec) { - throw new UnsupportedOperationException(); - } + registerPublicKeyImporter(HqcPublicKeySpec.class, new PublicKeyImporter<>() { @Override public PublicKey importPublic(HqcPublicKeySpec spec) throws GeneralSecurityException { @@ -237,31 +227,22 @@ public final class HqcAlgorithm extends AbstractCryptoAlgorithm { KeyFactory kf = KeyFactory.getInstance("HQC", providerName()); return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); } + }); - @Override - public PrivateKey importPrivate(HqcPublicKeySpec spec) { - throw new UnsupportedOperationException(); - } - }, null); - - registerAsymmetricKeyBuilder(HqcPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(HqcPrivateKeySpec spec) { - throw new UnsupportedOperationException(); - } - - @Override - public PublicKey importPublic(HqcPrivateKeySpec spec) { - throw new UnsupportedOperationException(); - } + registerPrivateKeyImporter(HqcPrivateKeySpec.class, new PrivateKeyImporter<>() { @Override public PrivateKey importPrivate(HqcPrivateKeySpec spec) throws GeneralSecurityException { ensureProvider(); KeyFactory kf = KeyFactory.getInstance("HQC", providerName()); - return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + byte[] encoded = spec.pkcs8(); + try { + return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } finally { + Arrays.fill(encoded, (byte) 0); + } } - }, null); + }); } private static void ensureProvider() throws NoSuchProviderException { diff --git a/lib/src/main/java/zeroecho/core/alg/hqc/HqcKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/hqc/HqcKeyGenSpec.java index 51de587..e7ab466 100644 --- a/lib/src/main/java/zeroecho/core/alg/hqc/HqcKeyGenSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/hqc/HqcKeyGenSpec.java @@ -65,7 +65,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * * // Generate a key pair via HqcAlgorithm * HqcAlgorithm hqc = new HqcAlgorithm(); - * KeyPair kp = hqc.generateKeyPair(spec); + * KeyPair kp = hqc.asymmetricKeyPairGenerator(HqcKeyGenSpec.class).generateKeyPair(spec); * }
      * * @since 1.0 diff --git a/lib/src/main/java/zeroecho/core/alg/hqc/HqcPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/hqc/HqcPrivateKeySpec.java index ba4dbfb..95314df 100644 --- a/lib/src/main/java/zeroecho/core/alg/hqc/HqcPrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/hqc/HqcPrivateKeySpec.java @@ -33,8 +33,12 @@ ******************************************************************************/ package zeroecho.core.alg.hqc; +import java.util.Arrays; import java.util.Base64; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq.Cursor; @@ -48,8 +52,8 @@ import zeroecho.core.spec.AlgorithmKeySpec; * *

      * This class is used to transport and import HQC private keys into the - * {@link HqcAlgorithm}. The encoded form is immutable and defensively copied on - * construction and retrieval. + * {@link HqcAlgorithm}. The encoded form is defensively copied on construction + * and retrieval and may be destroyed. *

      * *

      Serialization

      @@ -73,7 +77,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * * // Import into a PrivateKey via HqcAlgorithm * HqcAlgorithm hqc = new HqcAlgorithm(); - * PrivateKey priv = hqc.importPrivate(spec); + * PrivateKey priv = hqc.privateKeyImporter(HqcPrivateKeySpec.class).importPrivate(spec); * * // Serialize for transport * PairSeq serialized = HqcPrivateKeySpec.marshal(spec); @@ -84,10 +88,12 @@ import zeroecho.core.spec.AlgorithmKeySpec; * * @since 1.0 */ -public final class HqcPrivateKeySpec implements AlgorithmKeySpec { +public final class HqcPrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] pkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Constructs a new private key spec from a PKCS#8-encoded byte array. @@ -105,7 +111,13 @@ public final class HqcPrivateKeySpec implements AlgorithmKeySpec { * @return cloned PKCS#8 byte array */ public byte[] pkcs8() { - return pkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return pkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -122,7 +134,7 @@ public final class HqcPrivateKeySpec implements AlgorithmKeySpec { * @return serialized representation in a {@link PairSeq} */ public static PairSeq marshal(HqcPrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "HqcPrivateKeySpec", PKCS8_B64, b64); } @@ -144,7 +156,12 @@ public final class HqcPrivateKeySpec implements AlgorithmKeySpec { if (b64 == null) { throw new IllegalArgumentException("HqcPrivateKeySpec: missing pkcs8.b64"); } - return new HqcPrivateKeySpec(Base64.getDecoder().decode(b64)); + byte[] decoded = Base64.getDecoder().decode(b64); + try { + return new HqcPrivateKeySpec(decoded); + } finally { + Arrays.fill(decoded, (byte) 0); + } } /** @@ -160,4 +177,43 @@ public final class HqcPrivateKeySpec implements AlgorithmKeySpec { public String toString() { return "HqcPrivateKeySpec[len=" + pkcs8.length + "]"; } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(pkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(pkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("HQC private key specification has been destroyed"); + } + } } diff --git a/lib/src/main/java/zeroecho/core/alg/hqc/HqcPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/hqc/HqcPublicKeySpec.java index 04f3ecb..1a39b6c 100644 --- a/lib/src/main/java/zeroecho/core/alg/hqc/HqcPublicKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/hqc/HqcPublicKeySpec.java @@ -73,7 +73,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * * // Import into a PublicKey via HqcAlgorithm * HqcAlgorithm hqc = new HqcAlgorithm(); - * PublicKey pub = hqc.importPublic(spec); + * PublicKey pub = hqc.publicKeyImporter(HqcPublicKeySpec.class).importPublic(spec); * * // Serialize for transport * PairSeq serialized = HqcPublicKeySpec.marshal(spec); diff --git a/lib/src/main/java/zeroecho/core/alg/hqc/package-info.java b/lib/src/main/java/zeroecho/core/alg/hqc/package-info.java index 5ed9887..bf19067 100644 --- a/lib/src/main/java/zeroecho/core/alg/hqc/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/hqc/package-info.java @@ -49,8 +49,9 @@ * workflows. *
    75. Provide a {@link zeroecho.core.context.KemContext} bound to a public or * private key for encapsulation or decapsulation.
    76. - *
    77. Expose immutable specifications for key generation variants and encoded - * key carriers with simple marshalling helpers.
    78. + *
    79. Expose immutable key-generation specifications and defensively copying + * encoded-key carriers with simple marshalling helpers; private-key carriers + * are destroyable.
    80. *
    81. Ensure operations use a supported PQC provider and fail fast if the * provider is absent.
    82. * diff --git a/lib/src/main/java/zeroecho/core/alg/kyber/KyberAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/kyber/KyberAlgorithm.java index 09ea32f..084ba6e 100644 --- a/lib/src/main/java/zeroecho/core/alg/kyber/KyberAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/kyber/KyberAlgorithm.java @@ -44,6 +44,7 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; import java.security.spec.X509EncodedKeySpec; import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; @@ -56,7 +57,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; import zeroecho.core.context.KemContext; import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.spec.VoidSpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; /** * Concrete CryptoAlgorithm implementation for the post-quantum key @@ -112,22 +115,22 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * KyberAlgorithm kyber = new KyberAlgorithm(); * * // Generate a Kyber-768 key pair: - * KeyPair kp = kyber.asymmetricKeyBuilder(KyberKeyGenSpec.class) + * KeyPair kp = kyber.asymmetricKeyPairGenerator(KyberKeyGenSpec.class) * .generateKeyPair(KyberKeyGenSpec.kyber768()); * * // Encapsulation by initiator (recipient public key known): - * KemContext enc = kyber.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE); + * KemContext enc = kyber.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE); * * // Decapsulation by responder (own private key): - * KemContext dec = kyber.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE); + * KemContext dec = kyber.createContext(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE); * * // Message-style agreement (initiator): * MessageAgreementContext initCtx = - * kyber.create(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE); + * kyber.createContext(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE); * * // Message-style agreement (responder): * MessageAgreementContext respCtx = - * kyber.create(KeyUsage.AGREEMENT, kp.getPrivate(), VoidSpec.INSTANCE); + * kyber.createContext(KeyUsage.AGREEMENT, kp.getPrivate(), VoidSpec.INSTANCE); * } */ public final class KyberAlgorithm extends AbstractCryptoAlgorithm { @@ -154,7 +157,7 @@ public final class KyberAlgorithm extends AbstractCryptoAlgorithm { * Security.addProvider(new BouncyCastlePQCProvider()); * KyberAlgorithm alg = new KyberAlgorithm(); * - * KeyPair kp = alg.asymmetricKeyBuilder(KyberKeyGenSpec.class) + * KeyPair kp = alg.asymmetricKeyPairGenerator(KyberKeyGenSpec.class) * .generateKeyPair(KyberKeyGenSpec.kyber768()); * } */ @@ -189,7 +192,7 @@ public final class KyberAlgorithm extends AbstractCryptoAlgorithm { }, () -> VoidSpec.INSTANCE); // Keypair builder via BCPQC - registerAsymmetricKeyBuilder(KyberKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + registerAsymmetricKeyPairGenerator(KyberKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() { /** * Generates a Kyber key pair for the variant defined by the provided spec. * @@ -206,53 +209,10 @@ public final class KyberAlgorithm extends AbstractCryptoAlgorithm { kpg.initialize(params, new SecureRandom()); return kpg.generateKeyPair(); } - - /** - * Unsupported operation for this builder. Use {@code KyberPublicKeySpec} for - * public key import. - * - * @param spec the key generation spec; not used. - * @return never returns normally. - * @throws UnsupportedOperationException always thrown to indicate that public - * key import uses a dedicated encoded - * spec. - */ - @Override - public PublicKey importPublic(KyberKeyGenSpec spec) { - throw new UnsupportedOperationException("Import with a dedicated encoded spec, if needed."); - } - - /** - * Unsupported operation for this builder. Use {@code KyberPrivateKeySpec} for - * private key import. - * - * @param spec the key generation spec; not used. - * @return never returns normally. - * @throws UnsupportedOperationException always thrown to indicate that private - * key import uses a dedicated encoded - * spec. - */ - @Override - public PrivateKey importPrivate(KyberKeyGenSpec spec) { - throw new UnsupportedOperationException("Import with a dedicated encoded spec, if needed."); - } }, KyberKeyGenSpec::kyber768); // Public-key import (X.509) - registerAsymmetricKeyBuilder(KyberPublicKeySpec.class, new AsymmetricKeyBuilder<>() { - /** - * Unsupported operation for this builder. Use {@code KyberKeyGenSpec} for - * generation. - * - * @param spec the public key spec; not used. - * @return never returns normally. - * @throws UnsupportedOperationException always thrown to indicate that key - * generation is not supported here. - */ - @Override - public KeyPair generateKeyPair(KyberPublicKeySpec spec) { - throw new UnsupportedOperationException("Use KyberKeyGenSpec for generation"); - } + registerPublicKeyImporter(KyberPublicKeySpec.class, new PublicKeyImporter<>() { /** * Imports a Kyber public key from an X.509 SubjectPublicKeyInfo encoding. @@ -269,52 +229,10 @@ public final class KyberAlgorithm extends AbstractCryptoAlgorithm { KeyFactory kf = KeyFactory.getInstance("Kyber", providerName()); return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); } - - /** - * Unsupported operation for this builder. Use {@code KyberPrivateKeySpec} for - * private key import. - * - * @param spec the public key spec; not used. - * @return never returns normally. - * @throws UnsupportedOperationException always thrown to indicate that private - * key import is not supported here. - */ - @Override - public PrivateKey importPrivate(KyberPublicKeySpec spec) { - throw new UnsupportedOperationException("Use KyberPrivateKeySpec for private key import"); - } - }, null // no default spec - ); + }); // Private-key import (PKCS#8) - registerAsymmetricKeyBuilder(KyberPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { - /** - * Unsupported operation for this builder. Use {@code KyberKeyGenSpec} for - * generation. - * - * @param spec the private key spec; not used. - * @return never returns normally. - * @throws UnsupportedOperationException always thrown to indicate that key - * generation is not supported here. - */ - @Override - public KeyPair generateKeyPair(KyberPrivateKeySpec spec) { - throw new UnsupportedOperationException("Use KyberKeyGenSpec for generation"); - } - - /** - * Unsupported operation for this builder. Use {@code KyberPublicKeySpec} for - * public key import. - * - * @param spec the private key spec; not used. - * @return never returns normally. - * @throws UnsupportedOperationException always thrown to indicate that public - * key import is not supported here. - */ - @Override - public PublicKey importPublic(KyberPrivateKeySpec spec) { - throw new UnsupportedOperationException("Use KyberPublicKeySpec for public key import"); - } + registerPrivateKeyImporter(KyberPrivateKeySpec.class, new PrivateKeyImporter<>() { /** * Imports a Kyber private key from a PKCS#8 PrivateKeyInfo encoding. @@ -328,9 +246,14 @@ public final class KyberAlgorithm extends AbstractCryptoAlgorithm { public PrivateKey importPrivate(KyberPrivateKeySpec spec) throws GeneralSecurityException { ensureProvider(); KeyFactory kf = KeyFactory.getInstance("Kyber", providerName()); - return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + byte[] encoded = spec.pkcs8(); + try { + return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } finally { + Arrays.fill(encoded, (byte) 0); + } } - }, null); + }); } /** diff --git a/lib/src/main/java/zeroecho/core/alg/kyber/KyberKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/kyber/KyberKeyGenSpec.java index 812e1b0..a56a929 100644 --- a/lib/src/main/java/zeroecho/core/alg/kyber/KyberKeyGenSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/kyber/KyberKeyGenSpec.java @@ -54,7 +54,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; *

      Usage

      Instances of this class are passed to the Kyber key builder to * select the desired parameter set:
      {@code
        * CryptoAlgorithm kyber = new KyberAlgorithm();
      - * KeyPair kp = kyber.asymmetricKeyBuilder(KyberKeyGenSpec.class)
      + * KeyPair kp = kyber.asymmetricKeyPairGenerator(KyberKeyGenSpec.class)
        *                   .generateKeyPair(KyberKeyGenSpec.kyber768());
        * }
      * @@ -63,7 +63,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; *

      * * @see KyberAlgorithm - * @see zeroecho.core.CryptoAlgorithm#generateKeyPair(zeroecho.core.spec.AlgorithmKeySpec) + * @see zeroecho.sdk.KeyBuilders.Asymmetric#generateKeyPair(String, AlgorithmKeySpec) */ public final class KyberKeyGenSpec implements AlgorithmKeySpec, Describable { /** diff --git a/lib/src/main/java/zeroecho/core/alg/kyber/KyberPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/kyber/KyberPrivateKeySpec.java index d2a08c0..5e088be 100644 --- a/lib/src/main/java/zeroecho/core/alg/kyber/KyberPrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/kyber/KyberPrivateKeySpec.java @@ -33,8 +33,12 @@ ******************************************************************************/ package zeroecho.core.alg.kyber; +import java.util.Arrays; import java.util.Base64; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.spec.AlgorithmKeySpec; @@ -43,7 +47,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * Specification wrapper for a Kyber (ML-KEM) private key encoded in PKCS#8. * *

      - * Instances of this class carry an immutable copy of the PKCS#8-encoded private + * Instances of this class carry an owned copy of the PKCS#8-encoded private * key bytes. They are used with {@link zeroecho.core.CryptoAlgorithm} key * builders to import keys into the provider’s native representation. *

      @@ -51,7 +55,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; *

      Encoding

      *
        *
      • Format: PKCS#8 DER encoding of a Kyber private key.
      • - *
      • Stored as a defensive clone to ensure immutability.
      • + *
      • Stored as a defensive clone.
      • *
      • Marshalling/unmarshalling supported via {@link PairSeq} with Base64 * encoding.
      • *
      @@ -60,7 +64,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * // Import a private key into a CryptoAlgorithm * byte[] pkcs8Bytes = ...; // obtained from storage * KyberPrivateKeySpec spec = new KyberPrivateKeySpec(pkcs8Bytes); - * PrivateKey k = kyberAlg.importPrivate(spec); + * PrivateKey k = kyberAlg.privateKeyImporter(KyberPrivateKeySpec.class).importPrivate(spec); * * // Serialize for persistence * PairSeq seq = KyberPrivateKeySpec.marshal(spec); @@ -70,16 +74,18 @@ import zeroecho.core.spec.AlgorithmKeySpec; * } * *

      - * This class is immutable and thread-safe. + * Access and destruction are synchronized. *

      * * @see KyberPublicKeySpec * @see KyberKeyGenSpec */ -public final class KyberPrivateKeySpec implements AlgorithmKeySpec { +public final class KyberPrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] pkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Creates a new spec from PKCS#8-encoded private key bytes. @@ -97,7 +103,13 @@ public final class KyberPrivateKeySpec implements AlgorithmKeySpec { * @return cloned byte array of PKCS#8 DER encoding */ public byte[] pkcs8() { - return pkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return pkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -114,7 +126,7 @@ public final class KyberPrivateKeySpec implements AlgorithmKeySpec { * @return key-value representation suitable for persistence */ public static PairSeq marshal(KyberPrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "KyberPrivateKey", PKCS8_B64, b64); } @@ -139,7 +151,12 @@ public final class KyberPrivateKeySpec implements AlgorithmKeySpec { if (pkcs8b64 == null) { throw new IllegalArgumentException("KyberPrivateKeySpec: missing 'pkcs8.b64'"); } - return new KyberPrivateKeySpec(Base64.getDecoder().decode(pkcs8b64)); + byte[] decoded = Base64.getDecoder().decode(pkcs8b64); + try { + return new KyberPrivateKeySpec(decoded); + } finally { + Arrays.fill(decoded, (byte) 0); + } } /** @@ -155,4 +172,43 @@ public final class KyberPrivateKeySpec implements AlgorithmKeySpec { public String toString() { return "KyberPrivateKeySpec[len=" + pkcs8.length + "]"; } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(pkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(pkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("Kyber private key specification has been destroyed"); + } + } } diff --git a/lib/src/main/java/zeroecho/core/alg/kyber/KyberPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/kyber/KyberPublicKeySpec.java index 40dfc1f..834010f 100644 --- a/lib/src/main/java/zeroecho/core/alg/kyber/KyberPublicKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/kyber/KyberPublicKeySpec.java @@ -62,7 +62,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * // Import a public key into a CryptoAlgorithm * byte[] x509Bytes = ...; // obtained from storage * KyberPublicKeySpec spec = new KyberPublicKeySpec(x509Bytes); - * PublicKey k = kyberAlg.importPublic(spec); + * PublicKey k = kyberAlg.publicKeyImporter(KyberPublicKeySpec.class).importPublic(spec); * * // Serialize for persistence * PairSeq seq = KyberPublicKeySpec.marshal(spec); diff --git a/lib/src/main/java/zeroecho/core/alg/kyber/package-info.java b/lib/src/main/java/zeroecho/core/alg/kyber/package-info.java index 7b9b8f4..eaee39b 100644 --- a/lib/src/main/java/zeroecho/core/alg/kyber/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/kyber/package-info.java @@ -50,8 +50,9 @@ * message-style agreement adapter where needed. *
    83. Provide a {@link zeroecho.core.context.KemContext} implementation bound * to either a public or private key for encapsulation or decapsulation.
    84. - *
    85. Expose immutable specifications for key generation variants and encoded - * key carriers with compact marshalling helpers.
    86. + *
    87. Expose immutable key-generation specifications and defensively copying + * encoded-key carriers with compact marshalling helpers; private-key carriers + * are destroyable.
    88. *
    89. Ensure operations are delegated to an available PQC provider and fail * fast if the provider is absent.
    90. * diff --git a/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaAlgorithm.java index 14ebe70..14560a0 100644 --- a/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaAlgorithm.java @@ -97,8 +97,9 @@ public final class MldsaAlgorithm extends AbstractCryptoAlgorithm { } }, () -> VoidSpec.INSTANCE); - registerAsymmetricKeyBuilder(MldsaKeyGenSpec.class, new MldsaKeyGenBuilder(), MldsaKeyGenSpec::defaultSpec); - registerAsymmetricKeyBuilder(MldsaPublicKeySpec.class, new MldsaPublicKeyBuilder(), null); - registerAsymmetricKeyBuilder(MldsaPrivateKeySpec.class, new MldsaPrivateKeyBuilder(), null); + registerAsymmetricKeyPairGenerator(MldsaKeyGenSpec.class, new MldsaKeyGenBuilder(), + MldsaKeyGenSpec::defaultSpec); + registerPublicKeyImporter(MldsaPublicKeySpec.class, new MldsaPublicKeyBuilder()); + registerPrivateKeyImporter(MldsaPrivateKeySpec.class, new MldsaPrivateKeyBuilder()); } } diff --git a/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaKeyGenBuilder.java index 630064a..47dfd92 100644 --- a/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaKeyGenBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaKeyGenBuilder.java @@ -40,7 +40,7 @@ import java.security.KeyPairGenerator; import java.util.Locale; import java.util.Objects; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; /** * Key pair builder for ML-DSA (FIPS 204) using the Bouncy Castle provider. @@ -54,7 +54,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * * @since 1.0 */ -public final class MldsaKeyGenBuilder implements AsymmetricKeyBuilder { +public final class MldsaKeyGenBuilder implements AsymmetricKeyPairGenerator { private static final String ALG_PURE = "MLDSA"; private static final String ALG_SHA512 = "SHA512withMLDSA"; @@ -83,30 +83,6 @@ public final class MldsaKeyGenBuilder implements AsymmetricKeyBuilder { +public final class MldsaPrivateKeyBuilder implements PrivateKeyImporter { private static final String ALG = "ML-DSA"; - /** - * Generation is not supported by this spec. - * - * @param spec encoded private key spec - * @return never returns normally - * @throws UnsupportedOperationException always - */ - @Override - public KeyPair generateKeyPair(MldsaPrivateKeySpec spec) { - throw new UnsupportedOperationException("Generation not supported by this spec."); - } - - /** - * Public key import is not supported by this spec. - * - * @param spec encoded private key spec - * @return never returns normally - * @throws UnsupportedOperationException always - */ - @Override - public PublicKey importPublic(MldsaPrivateKeySpec spec) { - throw new UnsupportedOperationException("Use MldsaPublicKeySpec for public keys."); - } - /** * Imports a private key from PKCS#8 encoding. * @@ -87,6 +62,11 @@ public final class MldsaPrivateKeyBuilder implements AsymmetricKeyBuilder - * {@code MldsaPrivateKeySpec} is an immutable value object that wraps a + * {@code MldsaPrivateKeySpec} is a destroyable value object that wraps a * PKCS#8-encoded ML-DSA private key together with the JCA provider name that * should be used when importing the key. *

      @@ -62,10 +66,12 @@ import zeroecho.core.spec.AlgorithmKeySpec; * * @since 1.0 */ -public final class MldsaPrivateKeySpec implements AlgorithmKeySpec { +public final class MldsaPrivateKeySpec implements AlgorithmKeySpec, Destroyable { private final byte[] encodedPkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); private final String providerName; + private boolean destroyed; /** * Creates a new specification using the default provider {@code "BC"}. @@ -98,7 +104,13 @@ public final class MldsaPrivateKeySpec implements AlgorithmKeySpec { * @return a copy of the encoded private key */ public byte[] encoded() { - return encodedPkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return encodedPkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -118,7 +130,7 @@ public final class MldsaPrivateKeySpec implements AlgorithmKeySpec { * @throws NullPointerException if {@code spec} is {@code null} */ public static PairSeq marshal(MldsaPrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "MLDSA-PRIV", "pkcs8.b64", b64, "provider", spec.providerName); } @@ -133,20 +145,74 @@ public final class MldsaPrivateKeySpec implements AlgorithmKeySpec { public static MldsaPrivateKeySpec unmarshal(PairSeq p) { byte[] out = null; String prov = "BC"; - PairSeq.Cursor c = p.cursor(); - while (c.next()) { - String k = c.key(); - String v = c.value(); - switch (k) { - case "pkcs8.b64" -> out = Base64.getDecoder().decode(v); - case "provider" -> prov = v; - default -> { + try { + PairSeq.Cursor c = p.cursor(); + while (c.next()) { + String k = c.key(); + String v = c.value(); + switch (k) { + case "pkcs8.b64" -> out = decodeReplacing(out, v); + case "provider" -> prov = v; + default -> { + } } } + if (out == null) { + throw new IllegalArgumentException("pkcs8.b64 missing for ML-DSA private key"); + } + return new MldsaPrivateKeySpec(out, prov); + } finally { + wipe(out); } - if (out == null) { - throw new IllegalArgumentException("pkcs8.b64 missing for ML-DSA private key"); + } + + private static byte[] decodeReplacing(byte[] current, String encoded) { + wipe(current); + return Base64.getDecoder().decode(encoded); + } + + private static void wipe(byte[] current) { + if (current != null) { + Arrays.fill(current, (byte) 0); + } + } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(encodedPkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(encodedPkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("ML-DSA private key specification has been destroyed"); } - return new MldsaPrivateKeySpec(out, prov); } } diff --git a/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaPublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaPublicKeyBuilder.java index bb3795e..7ccc4a6 100644 --- a/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaPublicKeyBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/mldsa/MldsaPublicKeyBuilder.java @@ -35,34 +35,20 @@ package zeroecho.core.alg.mldsa; import java.security.GeneralSecurityException; import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.PrivateKey; import java.security.PublicKey; import java.security.spec.X509EncodedKeySpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.PublicKeyImporter; /** * Builder for importing ML-DSA public keys from encoded specifications. * * @since 1.0 */ -public final class MldsaPublicKeyBuilder implements AsymmetricKeyBuilder { +public final class MldsaPublicKeyBuilder implements PublicKeyImporter { private static final String ALG = "ML-DSA"; - /** - * Generation is not supported by this spec. - * - * @param spec encoded public key spec - * @return never returns normally - * @throws UnsupportedOperationException always - */ - @Override - public KeyPair generateKeyPair(MldsaPublicKeySpec spec) { - throw new UnsupportedOperationException("Generation not supported by this spec."); - } - /** * Imports a public key from X.509 encoding. * @@ -77,16 +63,4 @@ public final class MldsaPublicKeyBuilder implements AsymmetricKeyBuilder{@code * NtruAlgorithm alg = new NtruAlgorithm(); * // Generate a key pair (or import one). hrss701 is the default. - * KeyPair kp = alg.asymmetricKeyBuilder(NtruKeyGenSpec.class) + * KeyPair kp = alg.asymmetricKeyPairGenerator(NtruKeyGenSpec.class) * .generateKeyPair(NtruKeyGenSpec.hrss701()); * * // Initiator encapsulates using recipient's public key - * KemContext enc = alg.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE); + * KemContext enc = alg.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE); * KemResult kem = enc.encapsulate(); * * // Responder decapsulates using their private key - * KemContext dec = alg.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE); + * KemContext dec = alg.createContext(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE); * byte[] secret = dec.decapsulate(kem.encapsulation()); * } * @@ -107,18 +110,18 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; *

      *
      {@code
        * NtruAlgorithm alg = new NtruAlgorithm();
      - * KeyPair kpBob = alg.asymmetricKeyBuilder(NtruKeyGenSpec.class)
      + * KeyPair kpBob = alg.asymmetricKeyPairGenerator(NtruKeyGenSpec.class)
        *                    .generateKeyPair(NtruKeyGenSpec.hrss701());
        *
        * // Alice (initiator) - has Bob's public key
      - * MessageAgreementContext alice = alg.create(
      + * MessageAgreementContext alice = alg.createContext(
        *     KeyUsage.AGREEMENT, kpBob.getPublic(), VoidSpec.INSTANCE);
        *
        * // Alice produces the peer message she must send to Bob
        * byte[] toBob = alice.getPeerMessage();
        *
        * // Bob (responder) - has his private key
      - * MessageAgreementContext bob = alg.create(
      + * MessageAgreementContext bob = alg.createContext(
        *     KeyUsage.AGREEMENT, kpBob.getPrivate(), VoidSpec.INSTANCE);
        *
        * // Bob supplies Alice's message so he can complete decapsulation
      @@ -200,7 +203,7 @@ public final class NtruAlgorithm extends AbstractCryptoAlgorithm {
                       }, () -> VoidSpec.INSTANCE);
       
               // Keypair builder via BCPQC
      -        registerAsymmetricKeyBuilder(NtruKeyGenSpec.class, new AsymmetricKeyBuilder<>() {
      +        registerAsymmetricKeyPairGenerator(NtruKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
                   /**
                    * Generates a new NTRU key pair using the requested parameter set.
                    *
      @@ -217,45 +220,10 @@ public final class NtruAlgorithm extends AbstractCryptoAlgorithm {
                       kpg.initialize(params, new SecureRandom());
                       return kpg.generateKeyPair();
                   }
      -
      -            /**
      -             * Importing a public key is not supported by this spec type.
      -             *
      -             * @param spec unused
      -             * @return never returns
      -             * @throws UnsupportedOperationException always thrown
      -             */
      -            @Override
      -            public PublicKey importPublic(NtruKeyGenSpec spec) {
      -                throw new UnsupportedOperationException("Import with a dedicated encoded spec, if needed.");
      -            }
      -
      -            /**
      -             * Importing a private key is not supported by this spec type.
      -             *
      -             * @param spec unused
      -             * @return never returns
      -             * @throws UnsupportedOperationException always thrown
      -             */
      -            @Override
      -            public PrivateKey importPrivate(NtruKeyGenSpec spec) {
      -                throw new UnsupportedOperationException("Import with a dedicated encoded spec, if needed.");
      -            }
               }, NtruKeyGenSpec::hrss701); // sensible default
       
               // Public-key import (X.509)
      -        registerAsymmetricKeyBuilder(NtruPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
      -            /**
      -             * Key generation is not supported for an encoded public-key spec.
      -             *
      -             * @param spec unused
      -             * @return never returns
      -             * @throws UnsupportedOperationException always thrown
      -             */
      -            @Override
      -            public KeyPair generateKeyPair(NtruPublicKeySpec spec) {
      -                throw new UnsupportedOperationException("Use NtruKeyGenSpec for generation");
      -            }
      +        registerPublicKeyImporter(NtruPublicKeySpec.class, new PublicKeyImporter<>() {
       
                   /**
                    * Imports a public key from an X.509 SubjectPublicKeyInfo blob.
      @@ -270,45 +238,10 @@ public final class NtruAlgorithm extends AbstractCryptoAlgorithm {
                       KeyFactory kf = KeyFactory.getInstance("NTRU", providerName());
                       return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
                   }
      -
      -            /**
      -             * Private key import is not supported by the public-key spec.
      -             *
      -             * @param spec unused
      -             * @return never returns
      -             * @throws UnsupportedOperationException always thrown
      -             */
      -            @Override
      -            public PrivateKey importPrivate(NtruPublicKeySpec spec) {
      -                throw new UnsupportedOperationException("Use NtruPrivateKeySpec for private key import");
      -            }
      -        }, null);
      +        });
       
               // Private-key import (PKCS#8)
      -        registerAsymmetricKeyBuilder(NtruPrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
      -            /**
      -             * Key generation is not supported for an encoded private-key spec.
      -             *
      -             * @param spec unused
      -             * @return never returns
      -             * @throws UnsupportedOperationException always thrown
      -             */
      -            @Override
      -            public KeyPair generateKeyPair(NtruPrivateKeySpec spec) {
      -                throw new UnsupportedOperationException("Use NtruKeyGenSpec for generation");
      -            }
      -
      -            /**
      -             * Public key import is not supported by the private-key spec.
      -             *
      -             * @param spec unused
      -             * @return never returns
      -             * @throws UnsupportedOperationException always thrown
      -             */
      -            @Override
      -            public PublicKey importPublic(NtruPrivateKeySpec spec) {
      -                throw new UnsupportedOperationException("Use NtruPublicKeySpec for public key import");
      -            }
      +        registerPrivateKeyImporter(NtruPrivateKeySpec.class, new PrivateKeyImporter<>() {
       
                   /**
                    * Imports a private key from a PKCS#8 PrivateKeyInfo blob.
      @@ -321,9 +254,14 @@ public final class NtruAlgorithm extends AbstractCryptoAlgorithm {
                   public PrivateKey importPrivate(NtruPrivateKeySpec spec) throws GeneralSecurityException {
                       ensureProvider();
                       KeyFactory kf = KeyFactory.getInstance("NTRU", providerName());
      -                return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8()));
      +                byte[] encoded = spec.pkcs8();
      +                try {
      +                    return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
      +                } finally {
      +                    Arrays.fill(encoded, (byte) 0);
      +                }
                   }
      -        }, null);
      +        });
           }
       
           /**
      diff --git a/lib/src/main/java/zeroecho/core/alg/ntru/NtruKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/ntru/NtruKeyGenSpec.java
      index 8fa82bc..710a23b 100644
      --- a/lib/src/main/java/zeroecho/core/alg/ntru/NtruKeyGenSpec.java
      +++ b/lib/src/main/java/zeroecho/core/alg/ntru/NtruKeyGenSpec.java
      @@ -49,7 +49,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
        * 

      Usage

      {@code
        * // Choose a parameter set and generate a key pair
        * NtruAlgorithm alg = new NtruAlgorithm();
      - * KeyPair kp = alg.asymmetricKeyBuilder(NtruKeyGenSpec.class)
      + * KeyPair kp = alg.asymmetricKeyPairGenerator(NtruKeyGenSpec.class)
        *                 .generateKeyPair(NtruKeyGenSpec.hrss701());
        * }
      * diff --git a/lib/src/main/java/zeroecho/core/alg/ntru/NtruPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ntru/NtruPrivateKeySpec.java index 3d357cf..babd57b 100644 --- a/lib/src/main/java/zeroecho/core/alg/ntru/NtruPrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/ntru/NtruPrivateKeySpec.java @@ -33,8 +33,12 @@ ******************************************************************************/ package zeroecho.core.alg.ntru; +import java.util.Arrays; import java.util.Base64; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.spec.AlgorithmKeySpec; @@ -44,8 +48,9 @@ import zeroecho.core.spec.AlgorithmKeySpec; * simple marshal/unmarshal form. * *

      - * Instances are immutable. The byte array provided to the constructor is - * defensively copied and {@link #pkcs8()} returns a fresh clone on each call. + * The byte array provided to the constructor is defensively copied and + * {@link #pkcs8()} returns a fresh clone on each call. Access and destruction + * are synchronized. *

      * *

      Usage

      {@code
      @@ -66,10 +71,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
        *
        * @since 1.0
        */
      -public final class NtruPrivateKeySpec implements AlgorithmKeySpec {
      +public final class NtruPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
       
           private static final String PKCS8_B64 = "pkcs8.b64";
           private final byte[] pkcs8;
      +    private final ReentrantLock lifecycleLock = new ReentrantLock();
      +    private boolean destroyed;
       
           /**
            * Creates a new specification from PKCS#8-encoded bytes.
      @@ -96,7 +103,13 @@ public final class NtruPrivateKeySpec implements AlgorithmKeySpec {
            * @return a new byte array containing the PKCS#8 DER encoding
            */
           public byte[] pkcs8() {
      -        return pkcs8.clone();
      +        lifecycleLock.lock();
      +        try {
      +            ensureActive();
      +            return pkcs8.clone();
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
           }
       
           /**
      @@ -118,7 +131,7 @@ public final class NtruPrivateKeySpec implements AlgorithmKeySpec {
            * @throws NullPointerException if {@code spec} is null
            */
           public static PairSeq marshal(NtruPrivateKeySpec spec) {
      -        String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
      +        String b64 = spec.encodedKey();
               return PairSeq.of("type", "NtruPrivateKey", PKCS8_B64, b64);
           }
       
      @@ -151,7 +164,12 @@ public final class NtruPrivateKeySpec implements AlgorithmKeySpec {
               if (pkcs8b64 == null) {
                   throw new IllegalArgumentException("NtruPrivateKeySpec: missing 'pkcs8.b64'");
               }
      -        return new NtruPrivateKeySpec(Base64.getDecoder().decode(pkcs8b64));
      +        byte[] decoded = Base64.getDecoder().decode(pkcs8b64);
      +        try {
      +            return new NtruPrivateKeySpec(decoded);
      +        } finally {
      +            Arrays.fill(decoded, (byte) 0);
      +        }
           }
       
           /**
      @@ -163,4 +181,43 @@ public final class NtruPrivateKeySpec implements AlgorithmKeySpec {
           public String toString() {
               return "NtruPrivateKeySpec[len=" + pkcs8.length + "]";
           }
      +
      +    private String encodedKey() {
      +        lifecycleLock.lock();
      +        try {
      +            ensureActive();
      +            return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
      +    }
      +
      +    @Override
      +    public void destroy() {
      +        lifecycleLock.lock();
      +        try {
      +            if (!destroyed) {
      +                Arrays.fill(pkcs8, (byte) 0);
      +                destroyed = true;
      +            }
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
      +    }
      +
      +    @Override
      +    public boolean isDestroyed() {
      +        lifecycleLock.lock();
      +        try {
      +            return destroyed;
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
      +    }
      +
      +    private void ensureActive() {
      +        if (destroyed) {
      +            throw new IllegalStateException("NTRU private key specification has been destroyed");
      +        }
      +    }
       }
      diff --git a/lib/src/main/java/zeroecho/core/alg/ntru/package-info.java b/lib/src/main/java/zeroecho/core/alg/ntru/package-info.java
      index 08dc960..949976a 100644
      --- a/lib/src/main/java/zeroecho/core/alg/ntru/package-info.java
      +++ b/lib/src/main/java/zeroecho/core/alg/ntru/package-info.java
      @@ -48,8 +48,9 @@
        * DECAPSULATE roles, with an optional message-style agreement adapter.
        * 
    91. Provide a {@link zeroecho.core.context.KemContext} bound to either a * public key (encapsulation) or a private key (decapsulation).
    92. - *
    93. Expose immutable specifications for key generation variants and encoded - * key carriers with simple marshalling helpers.
    94. + *
    95. Expose immutable key-generation specifications and defensively copying + * encoded-key carriers with simple marshalling helpers; private-key carriers + * are destroyable.
    96. *
    97. Validate the presence of a suitable PQC provider before performing JCA * operations.
    98. * diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeAlgorithm.java index 642dd06..e34e846 100644 --- a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeAlgorithm.java @@ -44,6 +44,7 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; import java.security.spec.X509EncodedKeySpec; import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; @@ -56,7 +57,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; import zeroecho.core.context.KemContext; import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.spec.VoidSpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; /** * Configures and exposes the NTRU LPRime post-quantum KEM and a @@ -93,17 +96,16 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; *

      Example

      {@code
        * // Initialize the algorithm and generate a key pair
        * NtrulPrimeAlgorithm alg = new NtrulPrimeAlgorithm();
      - * KeyPair kp = alg.keys(NtrulPrimeKeyGenSpec.ntrulpr761()).generateKeyPair(NtrulPrimeKeyGenSpec.ntrulpr761());
      + * KeyPair kp = alg.asymmetricKeyPairGenerator(NtrulPrimeKeyGenSpec.class)
      + *                 .generateKeyPair(NtrulPrimeKeyGenSpec.ntrulpr761());
        *
        * // Initiator (Alice) encapsulates to Bob's public key
      - * MessageAgreementContext alice = alg
      - *     .context(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class,
      - *              kp.getPublic(), VoidSpec.INSTANCE);
      + * MessageAgreementContext alice =
      + *     alg.createContext(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE);
        *
        * // Responder (Bob) decapsulates with his private key
      - * MessageAgreementContext bob = alg
      - *     .context(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class,
      - *              kp.getPrivate(), VoidSpec.INSTANCE);
      + * MessageAgreementContext bob =
      + *     alg.createContext(KeyUsage.AGREEMENT, kp.getPrivate(), VoidSpec.INSTANCE);
        * }
      * * @see KemContext @@ -158,7 +160,7 @@ public final class NtrulPrimeAlgorithm extends AbstractCryptoAlgorithm { .asResponder().build(); }, () -> VoidSpec.INSTANCE); - registerAsymmetricKeyBuilder(NtrulPrimeKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + registerAsymmetricKeyPairGenerator(NtrulPrimeKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() { @Override public KeyPair generateKeyPair(NtrulPrimeKeyGenSpec spec) throws GeneralSecurityException { ensureProvider(); @@ -174,23 +176,9 @@ public final class NtrulPrimeAlgorithm extends AbstractCryptoAlgorithm { kpg.initialize(params, new SecureRandom()); return kpg.generateKeyPair(); } - - @Override - public PublicKey importPublic(NtrulPrimeKeyGenSpec spec) { - throw new UnsupportedOperationException(); - } - - @Override - public PrivateKey importPrivate(NtrulPrimeKeyGenSpec spec) { - throw new UnsupportedOperationException(); - } }, NtrulPrimeKeyGenSpec::ntrulpr1277); - registerAsymmetricKeyBuilder(NtrulPrimePublicKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(NtrulPrimePublicKeySpec spec) { - throw new UnsupportedOperationException(); - } + registerPublicKeyImporter(NtrulPrimePublicKeySpec.class, new PublicKeyImporter<>() { @Override public PublicKey importPublic(NtrulPrimePublicKeySpec spec) throws GeneralSecurityException { @@ -198,31 +186,22 @@ public final class NtrulPrimeAlgorithm extends AbstractCryptoAlgorithm { KeyFactory kf = KeyFactory.getInstance("NTRULPRime", providerName()); return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); } + }); - @Override - public PrivateKey importPrivate(NtrulPrimePublicKeySpec spec) { - throw new UnsupportedOperationException(); - } - }, null); - - registerAsymmetricKeyBuilder(NtrulPrimePrivateKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(NtrulPrimePrivateKeySpec spec) { - throw new UnsupportedOperationException(); - } - - @Override - public PublicKey importPublic(NtrulPrimePrivateKeySpec spec) { - throw new UnsupportedOperationException(); - } + registerPrivateKeyImporter(NtrulPrimePrivateKeySpec.class, new PrivateKeyImporter<>() { @Override public PrivateKey importPrivate(NtrulPrimePrivateKeySpec spec) throws GeneralSecurityException { ensureProvider(); KeyFactory kf = KeyFactory.getInstance("NTRULPRime", providerName()); - return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + byte[] encoded = spec.pkcs8(); + try { + return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } finally { + Arrays.fill(encoded, (byte) 0); + } } - }, null); + }); } /** diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeKeyGenSpec.java index 60241d1..0a379bf 100644 --- a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeKeyGenSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeKeyGenSpec.java @@ -57,15 +57,14 @@ import zeroecho.core.spec.AlgorithmKeySpec; * * *

      - * Instances are immutable and typically passed to - * {@link zeroecho.core.CryptoAlgorithm#generateKeyPair} or retrieved from a - * {@code CryptoAlgorithm} builder. For convenience, static factory methods are - * provided for each variant. + * Instances are immutable and passed to + * {@link zeroecho.sdk.KeyBuilders.Asymmetric#generateKeyPair(String, AlgorithmKeySpec)}. + * Static factory methods are provided for each variant. *

      * *

      Example

      {@code
      - * CryptoAlgorithm alg = CryptoAlgorithms.require("NTRULPRime");
      - * KeyPair kp = alg.generateKeyPair(NtrulPrimeKeyGenSpec.ntrulpr761());
      + * KeyPair kp = session.keyBuilders().asymmetric()
      + *         .generateKeyPair("NTRULPRime", NtrulPrimeKeyGenSpec.ntrulpr761());
        * }
      * * @since 1.0 diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePrivateKeySpec.java index 752fcb2..ed426a9 100644 --- a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePrivateKeySpec.java @@ -33,8 +33,12 @@ ******************************************************************************/ package zeroecho.core.alg.ntruprime; +import java.util.Arrays; import java.util.Base64; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq.Cursor; @@ -47,7 +51,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; *

      * {@code NtrulPrimePrivateKeySpec} provides a type-safe holder for * PKCS#8-encoded private key material belonging to the NTRU LPRime KEM. It is - * immutable and defensive copies are made on construction and retrieval. + * destroyable, and defensive copies are made on construction and retrieval. *

      * *

      Usage

      Instances are typically created after parsing or receiving @@ -60,7 +64,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * NtrulPrimePrivateKeySpec spec = new NtrulPrimePrivateKeySpec(pkcs8); * * // Import via CryptoAlgorithms - * PrivateKey priv = CryptoAlgorithms.privateKey("NTRULPRime", spec); + * PrivateKey priv = session.keyBuilders().asymmetric().importPrivate("NTRULPRime", spec); * }
      * *

      Serialization

      A lightweight marshaling format is supported via @@ -74,10 +78,12 @@ import zeroecho.core.spec.AlgorithmKeySpec; * * @since 1.0 */ -public final class NtrulPrimePrivateKeySpec implements AlgorithmKeySpec { +public final class NtrulPrimePrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] pkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Constructs a new private key specification from a PKCS#8-encoded byte array. @@ -100,7 +106,13 @@ public final class NtrulPrimePrivateKeySpec implements AlgorithmKeySpec { * @return copy of the encoded private key */ public byte[] pkcs8() { - return pkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return pkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -117,7 +129,7 @@ public final class NtrulPrimePrivateKeySpec implements AlgorithmKeySpec { * @return serialized representation containing the base64-encoded key */ public static PairSeq marshal(NtrulPrimePrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "NtrulPrimePrivateKeySpec", PKCS8_B64, b64); } @@ -144,7 +156,12 @@ public final class NtrulPrimePrivateKeySpec implements AlgorithmKeySpec { if (b64 == null) { throw new IllegalArgumentException("NtrulPrimePrivateKeySpec: missing pkcs8.b64"); } - return new NtrulPrimePrivateKeySpec(Base64.getDecoder().decode(b64)); + byte[] decoded = Base64.getDecoder().decode(b64); + try { + return new NtrulPrimePrivateKeySpec(decoded); + } finally { + Arrays.fill(decoded, (byte) 0); + } } /** @@ -160,4 +177,43 @@ public final class NtrulPrimePrivateKeySpec implements AlgorithmKeySpec { public String toString() { return "NtrulPrimePrivateKeySpec[len=" + pkcs8.length + "]"; } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(pkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(pkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("NTRU LPRime private key specification has been destroyed"); + } + } } diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePublicKeySpec.java index 128be81..15a84ea 100644 --- a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePublicKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePublicKeySpec.java @@ -59,7 +59,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * NtrulPrimePublicKeySpec spec = new NtrulPrimePublicKeySpec(der); * * // Import via CryptoAlgorithms - * PublicKey pub = CryptoAlgorithms.publicKey("NTRULPRime", spec); + * PublicKey pub = session.keyBuilders().asymmetric().importPublic("NTRULPRime", spec); * }
      * *

      Serialization

      A lightweight marshaling format is supported via diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeAlgorithm.java index c17edd5..4c777bd 100644 --- a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeAlgorithm.java @@ -44,6 +44,7 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; import java.security.spec.X509EncodedKeySpec; import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; @@ -56,7 +57,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; import zeroecho.core.context.KemContext; import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.spec.VoidSpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; /** * Configures and exposes the SNTRU Prime post-quantum KEM and a @@ -93,7 +96,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; *

      Example

      {@code
        * // Initialize the algorithm and generate a key pair
        * SntruPrimeAlgorithm alg = new SntruPrimeAlgorithm();
      - * KeyPair kp = alg.keys(SntruPrimeKeyGenSpec.sntrup761())
      + * KeyPair kp = alg.asymmetricKeyPairGenerator(SntruPrimeKeyGenSpec.class)
        *                 .generateKeyPair(SntruPrimeKeyGenSpec.sntrup761());
        *
        * // Initiator (Alice) encapsulates to Bob's public key
      @@ -159,7 +162,7 @@ public final class SntruPrimeAlgorithm extends AbstractCryptoAlgorithm {
                                   .asResponder().build();
                       }, () -> VoidSpec.INSTANCE);
       
      -        registerAsymmetricKeyBuilder(SntruPrimeKeyGenSpec.class, new AsymmetricKeyBuilder<>() {
      +        registerAsymmetricKeyPairGenerator(SntruPrimeKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
                   @Override
                   public KeyPair generateKeyPair(SntruPrimeKeyGenSpec spec) throws GeneralSecurityException {
                       ensureProvider();
      @@ -176,23 +179,9 @@ public final class SntruPrimeAlgorithm extends AbstractCryptoAlgorithm {
                       kpg.initialize(params, new SecureRandom());
                       return kpg.generateKeyPair();
                   }
      -
      -            @Override
      -            public PublicKey importPublic(SntruPrimeKeyGenSpec spec) {
      -                throw new UnsupportedOperationException();
      -            }
      -
      -            @Override
      -            public PrivateKey importPrivate(SntruPrimeKeyGenSpec spec) {
      -                throw new UnsupportedOperationException();
      -            }
               }, SntruPrimeKeyGenSpec::sntrup1277);
       
      -        registerAsymmetricKeyBuilder(SntruPrimePublicKeySpec.class, new AsymmetricKeyBuilder<>() {
      -            @Override
      -            public KeyPair generateKeyPair(SntruPrimePublicKeySpec spec) {
      -                throw new UnsupportedOperationException();
      -            }
      +        registerPublicKeyImporter(SntruPrimePublicKeySpec.class, new PublicKeyImporter<>() {
       
                   @Override
                   public PublicKey importPublic(SntruPrimePublicKeySpec spec) throws GeneralSecurityException {
      @@ -200,31 +189,22 @@ public final class SntruPrimeAlgorithm extends AbstractCryptoAlgorithm {
                       KeyFactory kf = KeyFactory.getInstance("SNTRUPrime", providerName());
                       return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
                   }
      +        });
       
      -            @Override
      -            public PrivateKey importPrivate(SntruPrimePublicKeySpec spec) {
      -                throw new UnsupportedOperationException();
      -            }
      -        }, null);
      -
      -        registerAsymmetricKeyBuilder(SntruPrimePrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
      -            @Override
      -            public KeyPair generateKeyPair(SntruPrimePrivateKeySpec spec) {
      -                throw new UnsupportedOperationException();
      -            }
      -
      -            @Override
      -            public PublicKey importPublic(SntruPrimePrivateKeySpec spec) {
      -                throw new UnsupportedOperationException();
      -            }
      +        registerPrivateKeyImporter(SntruPrimePrivateKeySpec.class, new PrivateKeyImporter<>() {
       
                   @Override
                   public PrivateKey importPrivate(SntruPrimePrivateKeySpec spec) throws GeneralSecurityException {
                       ensureProvider();
                       KeyFactory kf = KeyFactory.getInstance("SNTRUPrime", providerName());
      -                return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8()));
      +                byte[] encoded = spec.pkcs8();
      +                try {
      +                    return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
      +                } finally {
      +                    Arrays.fill(encoded, (byte) 0);
      +                }
                   }
      -        }, null);
      +        });
           }
       
           /**
      diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeKeyGenSpec.java
      index ffb3ffe..f060087 100644
      --- a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeKeyGenSpec.java
      +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeKeyGenSpec.java
      @@ -57,15 +57,14 @@ import zeroecho.core.spec.AlgorithmKeySpec;
        * 
        *
        * 

      - * Instances are immutable and typically passed to - * {@link zeroecho.core.CryptoAlgorithm#generateKeyPair} or retrieved from a - * {@code CryptoAlgorithm} builder. For convenience, static factory methods are - * provided for each variant. + * Instances are immutable and passed to + * {@link zeroecho.sdk.KeyBuilders.Asymmetric#generateKeyPair(String, AlgorithmKeySpec)}. + * Static factory methods are provided for each variant. *

      * *

      Example

      {@code
      - * CryptoAlgorithm alg = CryptoAlgorithms.require("SNTRUPrime");
      - * KeyPair kp = alg.generateKeyPair(SntruPrimeKeyGenSpec.sntrup761());
      + * KeyPair kp = session.keyBuilders().asymmetric()
      + *         .generateKeyPair("SNTRUPrime", SntruPrimeKeyGenSpec.sntrup761());
        * }
      * * @since 1.0 diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePrivateKeySpec.java index 3f33d7f..9b4f0d4 100644 --- a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePrivateKeySpec.java @@ -33,8 +33,12 @@ ******************************************************************************/ package zeroecho.core.alg.ntruprime; +import java.util.Arrays; import java.util.Base64; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq.Cursor; @@ -47,7 +51,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; *

      * {@code SntruPrimePrivateKeySpec} provides a type-safe holder for * PKCS#8-encoded private key material belonging to the SNTRU Prime KEM. It is - * immutable and defensive copies are made on construction and retrieval. + * destroyable, and defensive copies are made on construction and retrieval. *

      * *

      Usage

      Instances are typically created after parsing or receiving @@ -60,7 +64,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * SntruPrimePrivateKeySpec spec = new SntruPrimePrivateKeySpec(pkcs8); * * // Import via CryptoAlgorithms - * PrivateKey priv = CryptoAlgorithms.privateKey("SNTRUPrime", spec); + * PrivateKey priv = session.keyBuilders().asymmetric().importPrivate("SNTRUPrime", spec); * }
      * *

      Serialization

      A lightweight marshaling format is supported via @@ -74,10 +78,12 @@ import zeroecho.core.spec.AlgorithmKeySpec; * * @since 1.0 */ -public final class SntruPrimePrivateKeySpec implements AlgorithmKeySpec { +public final class SntruPrimePrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] pkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Constructs a new private key specification from a PKCS#8-encoded byte array. @@ -100,7 +106,13 @@ public final class SntruPrimePrivateKeySpec implements AlgorithmKeySpec { * @return copy of the encoded private key */ public byte[] pkcs8() { - return pkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return pkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -117,7 +129,7 @@ public final class SntruPrimePrivateKeySpec implements AlgorithmKeySpec { * @return serialized representation containing the base64-encoded key */ public static PairSeq marshal(SntruPrimePrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "SntruPrimePrivateKeySpec", PKCS8_B64, b64); } @@ -144,7 +156,12 @@ public final class SntruPrimePrivateKeySpec implements AlgorithmKeySpec { if (b64 == null) { throw new IllegalArgumentException("SntruPrimePrivateKeySpec: missing pkcs8.b64"); } - return new SntruPrimePrivateKeySpec(Base64.getDecoder().decode(b64)); + byte[] decoded = Base64.getDecoder().decode(b64); + try { + return new SntruPrimePrivateKeySpec(decoded); + } finally { + Arrays.fill(decoded, (byte) 0); + } } /** @@ -160,4 +177,43 @@ public final class SntruPrimePrivateKeySpec implements AlgorithmKeySpec { public String toString() { return "SntruPrimePrivateKeySpec[len=" + pkcs8.length + "]"; } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(pkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(pkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("SNTRU Prime private key specification has been destroyed"); + } + } } diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePublicKeySpec.java index c4b4f8b..fbfe6bd 100644 --- a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePublicKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePublicKeySpec.java @@ -59,7 +59,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * SntruPrimePublicKeySpec spec = new SntruPrimePublicKeySpec(der); * * // Import via CryptoAlgorithms - * PublicKey pub = CryptoAlgorithms.publicKey("SNTRUPrime", spec); + * PublicKey pub = session.keyBuilders().asymmetric().importPublic("SNTRUPrime", spec); * } * *

      Serialization

      A lightweight marshaling format is supported via diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/package-info.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/package-info.java index fe651ce..ccf8b73 100644 --- a/lib/src/main/java/zeroecho/core/alg/ntruprime/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/package-info.java @@ -49,8 +49,9 @@ * a message-style initiator/responder API. *
    99. Provide {@link zeroecho.core.context.KemContext} implementations bound to * a public key (encapsulation) or a private key (decapsulation).
    100. - *
    101. Expose immutable specifications for key generation variants and - * encoded-key carriers with compact marshalling helpers.
    102. + *
    103. Expose immutable key-generation specifications and defensively copying + * encoded-key carriers with compact marshalling helpers; private-key carriers + * are destroyable.
    104. *
    105. Validate provider availability and fail fast if the PQC provider is * absent.
    106. * diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/BlockGeometry.java b/lib/src/main/java/zeroecho/core/alg/rsa/BlockGeometry.java index 07acae4..81b6e55 100644 --- a/lib/src/main/java/zeroecho/core/alg/rsa/BlockGeometry.java +++ b/lib/src/main/java/zeroecho/core/alg/rsa/BlockGeometry.java @@ -36,6 +36,7 @@ package zeroecho.core.alg.rsa; import java.math.BigInteger; import java.security.Key; import java.security.interfaces.RSAKey; +import java.util.Objects; /** * Describes block sizing rules for RSA encryption and decryption. @@ -70,18 +71,13 @@ import java.security.interfaces.RSAKey; * RsaEncSpec spec = RsaEncSpec.ofPkcs1v15(); * BlockGeometry g = BlockGeometry.forRsa(spec, pub, true); * System.out.printf("Plaintext block: %d, Ciphertext block: %d%n", - * g.inChunkSize, g.outChunkSize); + * g.inChunkSize(), g.outChunkSize()); * } * * @since 1.0 */ -public final class BlockGeometry { - /** Number of input bytes per block. */ - public final int inChunkSize; - /** Number of output bytes per block. */ - public final int outChunkSize; - /** Extra chunks produced during finalization (always 0 for RSA). */ - public final int finalizationOutputChunks; +public record BlockGeometry(int inChunkSize, int outChunkSize, int finalizationOutputChunks) { + private static final int MINIMUM_INPUT_CHUNK_SIZE = 2; /** * Constructs a new block geometry descriptor. @@ -90,11 +86,25 @@ public final class BlockGeometry { * @param outChunkSize output bytes per block * @param finalizationOutputChunks number of additional chunks during * finalization + * @throws IllegalArgumentException if input size is not greater than one, + * output size is not positive, input exceeds + * output, or finalization chunks is not zero */ - public BlockGeometry(int inChunkSize, int outChunkSize, int finalizationOutputChunks) { - this.inChunkSize = inChunkSize; - this.outChunkSize = outChunkSize; - this.finalizationOutputChunks = finalizationOutputChunks; + public BlockGeometry { + if (inChunkSize < MINIMUM_INPUT_CHUNK_SIZE) { + throw new IllegalArgumentException("inChunkSize must be greater than 1: " + inChunkSize); + } + if (outChunkSize <= 0) { + throw new IllegalArgumentException("outChunkSize must be positive: " + outChunkSize); + } + if (inChunkSize > outChunkSize) { + throw new IllegalArgumentException( + "inChunkSize must not exceed outChunkSize: " + inChunkSize + " > " + outChunkSize); + } + if (finalizationOutputChunks != 0) { + throw new IllegalArgumentException( + "finalizationOutputChunks must be zero: " + finalizationOutputChunks); + } } /** @@ -151,4 +161,27 @@ public final class BlockGeometry { case SHA512 -> 64; }; } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof BlockGeometry geometry)) { + return false; + } + return inChunkSize == geometry.inChunkSize && outChunkSize == geometry.outChunkSize + && finalizationOutputChunks == geometry.finalizationOutputChunks; + } + + @Override + public int hashCode() { + return Objects.hash(inChunkSize, outChunkSize, finalizationOutputChunks); + } + + @Override + public String toString() { + return "BlockGeometry[inChunkSize=" + inChunkSize + ", outChunkSize=" + outChunkSize + + ", finalizationOutputChunks=" + finalizationOutputChunks + "]"; + } } diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/RsaAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/rsa/RsaAlgorithm.java index 078c47b..c9f7ef8 100644 --- a/lib/src/main/java/zeroecho/core/alg/rsa/RsaAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/rsa/RsaAlgorithm.java @@ -43,13 +43,16 @@ import java.security.PublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.RSAKeyGenParameterSpec; import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; import zeroecho.core.AlgorithmFamily; import zeroecho.core.KeyUsage; import zeroecho.core.alg.AbstractCryptoAlgorithm; import zeroecho.core.context.EncryptionContext; import zeroecho.core.context.SignatureContext; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; /** * RSA algorithm binding for encryption/decryption and signature/verification. @@ -80,14 +83,14 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * RsaAlgorithm rsa = new RsaAlgorithm(); * * // Generate a new RSA-2048 key pair - * KeyPair kp = rsa.asymmetricKeyBuilder(RsaKeyGenSpec.class) + * KeyPair kp = rsa.asymmetricKeyPairGenerator(RsaKeyGenSpec.class) * .generateKeyPair(RsaKeyGenSpec.rsa2048()); * * // Encrypt with OAEP (SHA-256) - * EncryptionContext enc = rsa.create(KeyUsage.ENCRYPT, kp.getPublic(), null); + * EncryptionContext enc = rsa.createContext(KeyUsage.ENCRYPT, kp.getPublic(), null); * * // Sign with PSS (SHA-256) - * SignatureContext sig = rsa.create(KeyUsage.SIGN, kp.getPrivate(), null); + * SignatureContext sig = rsa.createContext(KeyUsage.SIGN, kp.getPrivate(), null); * } * *

      @@ -158,7 +161,7 @@ public final class RsaAlgorithm extends AbstractCryptoAlgorithm { }, () -> RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32)); // Key builders - registerAsymmetricKeyBuilder(RsaKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + registerAsymmetricKeyPairGenerator(RsaKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() { @Override public KeyPair generateKeyPair(RsaKeyGenSpec spec) throws GeneralSecurityException { KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); @@ -167,52 +170,29 @@ public final class RsaAlgorithm extends AbstractCryptoAlgorithm { kpg.initialize(params); return kpg.generateKeyPair(); } - - @Override - public PublicKey importPublic(RsaKeyGenSpec spec) { - throw new UnsupportedOperationException("Use RsaPublicKeySpec to import a public key."); - } - - @Override - public PrivateKey importPrivate(RsaKeyGenSpec spec) { - throw new UnsupportedOperationException("Use RsaPrivateKeySpec to import a private key."); - } }, RsaKeyGenSpec::rsa2048); - registerAsymmetricKeyBuilder(RsaPublicKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(RsaPublicKeySpec spec) { - throw new UnsupportedOperationException("Generation not supported for encoded spec."); - } + registerPublicKeyImporter(RsaPublicKeySpec.class, new PublicKeyImporter<>() { @Override public PublicKey importPublic(RsaPublicKeySpec spec) throws GeneralSecurityException { KeyFactory kf = KeyFactory.getInstance("RSA"); return kf.generatePublic(new X509EncodedKeySpec(spec.encoded())); } + }); - @Override - public PrivateKey importPrivate(RsaPublicKeySpec spec) { - throw new UnsupportedOperationException("Use RsaPrivateKeySpec for private keys."); - } - }, null); - - registerAsymmetricKeyBuilder(RsaPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(RsaPrivateKeySpec spec) { - throw new UnsupportedOperationException("Generation not supported for encoded spec."); - } - - @Override - public PublicKey importPublic(RsaPrivateKeySpec spec) { - throw new UnsupportedOperationException("Use RsaPublicKeySpec for public keys."); - } + registerPrivateKeyImporter(RsaPrivateKeySpec.class, new PrivateKeyImporter<>() { @Override public PrivateKey importPrivate(RsaPrivateKeySpec spec) throws GeneralSecurityException { KeyFactory kf = KeyFactory.getInstance("RSA"); - return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); + byte[] encoded = spec.encoded(); + try { + return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } finally { + Arrays.fill(encoded, (byte) 0); + } } - }, null); + }); } } diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/RsaCipherContext.java b/lib/src/main/java/zeroecho/core/alg/rsa/RsaCipherContext.java index fdc4798..603dac0 100644 --- a/lib/src/main/java/zeroecho/core/alg/rsa/RsaCipherContext.java +++ b/lib/src/main/java/zeroecho/core/alg/rsa/RsaCipherContext.java @@ -187,9 +187,9 @@ public final class RsaCipherContext implements EncryptionContext { BlockGeometry rsaGeometry = BlockGeometry.forRsa(spec, key, encrypt); return CipherTransformInputStreamBuilder.builder().withUpstream(upstream).withCipher(cipher) - .withInputBlockSize(rsaGeometry.inChunkSize).withOutputBlockSize(rsaGeometry.outChunkSize) - .withBufferedBlocks(100).withFinalizationOutputChunks(rsaGeometry.finalizationOutputChunks) - .withUpdateStreaming(false).build(); + .withInputBlockSize(rsaGeometry.inChunkSize()).withOutputBlockSize(rsaGeometry.outChunkSize()) + .withBufferedBlocks(100).withFinalizationOutputChunks(rsaGeometry.finalizationOutputChunks()) + .withIndependentBlocks().build(); } catch (GeneralSecurityException e) { throw new IOException(spec.description() + " RSA attach/init failed: " + e.getMessage(), e); } diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/RsaKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/rsa/RsaKeyGenSpec.java index 550ef4b..e92b350 100644 --- a/lib/src/main/java/zeroecho/core/alg/rsa/RsaKeyGenSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/rsa/RsaKeyGenSpec.java @@ -59,7 +59,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * RsaKeyGenSpec spec = RsaKeyGenSpec.rsa2048(); * * // Generate the key pair - * KeyPair kp = rsaAlgorithm.asymmetricKeyBuilder(RsaKeyGenSpec.class) + * KeyPair kp = rsaAlgorithm.asymmetricKeyPairGenerator(RsaKeyGenSpec.class) * .generateKeyPair(spec); * } * diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/RsaPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/rsa/RsaPrivateKeySpec.java index 7fbf36f..0f5cee0 100644 --- a/lib/src/main/java/zeroecho/core/alg/rsa/RsaPrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/rsa/RsaPrivateKeySpec.java @@ -33,7 +33,11 @@ ******************************************************************************/ package zeroecho.core.alg.rsa; +import java.util.Arrays; import java.util.Base64; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.spec.AlgorithmKeySpec; @@ -64,15 +68,18 @@ import zeroecho.core.spec.AlgorithmKeySpec; * } * *

      - * Instances are immutable and defensively copy their input. The encoded key - * material remains sensitive and should be handled with care. + * Instances defensively copy their input and may be destroyed to wipe the owned + * encoding. The encoded key material remains sensitive and should be handled + * with care. *

      * * @since 1.0 */ -public final class RsaPrivateKeySpec implements AlgorithmKeySpec { +public final class RsaPrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] pkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Constructs a new RSA private key spec from a PKCS#8-encoded key. @@ -90,7 +97,13 @@ public final class RsaPrivateKeySpec implements AlgorithmKeySpec { * @return defensive copy of the encoded key */ public byte[] encoded() { - return pkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return pkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -107,7 +120,7 @@ public final class RsaPrivateKeySpec implements AlgorithmKeySpec { * @return pair sequence with type and base64-encoded key material */ public static PairSeq marshal(RsaPrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "RSA-PRIV", PKCS8_B64, b64); } @@ -131,12 +144,62 @@ public final class RsaPrivateKeySpec implements AlgorithmKeySpec { String k = cur.key(); String v = cur.value(); if (PKCS8_B64.equals(k)) { - out = Base64.getDecoder().decode(v); + out = decodeReplacing(out, v); } } if (out == null) { throw new IllegalArgumentException("pkcs8.b64 missing for RSA private key"); } - return new RsaPrivateKeySpec(out); + try { + return new RsaPrivateKeySpec(out); + } finally { + Arrays.fill(out, (byte) 0); + } + } + + private static byte[] decodeReplacing(byte[] current, String encoded) { + if (current != null) { + Arrays.fill(current, (byte) 0); + } + return Base64.getDecoder().decode(encoded); + } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(pkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(pkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("RSA private key specification has been destroyed"); + } } } diff --git a/lib/src/main/java/zeroecho/core/alg/saber/SaberAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/saber/SaberAlgorithm.java index 5912134..70643d1 100644 --- a/lib/src/main/java/zeroecho/core/alg/saber/SaberAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/saber/SaberAlgorithm.java @@ -44,6 +44,7 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; import java.security.spec.X509EncodedKeySpec; import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; @@ -55,7 +56,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; import zeroecho.core.context.KemContext; import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.spec.VoidSpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; /** * Implements the SABER post-quantum key encapsulation mechanism for the crypto @@ -73,7 +76,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * message-oriented interface suitable for one-pass key agreement. * *

      Key material import and generation

      The algorithm registers - * {@code AsymmetricKeyBuilder} implementations for: + * operation-specific key implementations for: *
        *
      • {@code SaberKeyGenSpec}: generates SABER key pairs for the requested * parameter set using {@code KeyPairGenerator} from the provider.
      • @@ -92,14 +95,15 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * CryptoAlgorithm saber = new SaberAlgorithm(); * * // Generate a key pair for a specific SABER variant - * KeyPair kp = saber.build(SaberKeyGenSpec.saberkem256r3()).generateKeyPair(); + * KeyPair kp = saber.asymmetricKeyPairGenerator(SaberKeyGenSpec.class) + * .generateKeyPair(SaberKeyGenSpec.saberkem256r3()); * * // Encapsulation (initiator) - * KemContext enc = saber.create(KemContext.class, kp.getPublic(), VoidSpec.INSTANCE); + * KemContext enc = saber.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE); * KemResult out = enc.encapsulate(); * * // Decapsulation (responder) - * KemContext dec = saber.create(KemContext.class, kp.getPrivate(), VoidSpec.INSTANCE); + * KemContext dec = saber.createContext(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE); * SecretKey k = dec.decapsulate(out.ciphertext()); * } */ @@ -154,7 +158,7 @@ public final class SaberAlgorithm extends AbstractCryptoAlgorithm { .build(); }, () -> VoidSpec.INSTANCE); - registerAsymmetricKeyBuilder(SaberKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + registerAsymmetricKeyPairGenerator(SaberKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() { @Override public KeyPair generateKeyPair(SaberKeyGenSpec spec) throws GeneralSecurityException { ensureProvider(); @@ -176,23 +180,9 @@ public final class SaberAlgorithm extends AbstractCryptoAlgorithm { kpg.initialize(params, new SecureRandom()); return kpg.generateKeyPair(); } - - @Override - public PublicKey importPublic(SaberKeyGenSpec spec) { - throw new UnsupportedOperationException(); - } - - @Override - public PrivateKey importPrivate(SaberKeyGenSpec spec) { - throw new UnsupportedOperationException(); - } }, SaberKeyGenSpec::saberkem256r3); - registerAsymmetricKeyBuilder(SaberPublicKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(SaberPublicKeySpec spec) { - throw new UnsupportedOperationException(); - } + registerPublicKeyImporter(SaberPublicKeySpec.class, new PublicKeyImporter<>() { @Override public PublicKey importPublic(SaberPublicKeySpec spec) throws GeneralSecurityException { @@ -200,31 +190,22 @@ public final class SaberAlgorithm extends AbstractCryptoAlgorithm { KeyFactory kf = KeyFactory.getInstance("SABER", providerName()); return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); } + }); - @Override - public PrivateKey importPrivate(SaberPublicKeySpec spec) { - throw new UnsupportedOperationException(); - } - }, null); - - registerAsymmetricKeyBuilder(SaberPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(SaberPrivateKeySpec spec) { - throw new UnsupportedOperationException(); - } - - @Override - public PublicKey importPublic(SaberPrivateKeySpec spec) { - throw new UnsupportedOperationException(); - } + registerPrivateKeyImporter(SaberPrivateKeySpec.class, new PrivateKeyImporter<>() { @Override public PrivateKey importPrivate(SaberPrivateKeySpec spec) throws GeneralSecurityException { ensureProvider(); KeyFactory kf = KeyFactory.getInstance("SABER", providerName()); - return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + byte[] encoded = spec.pkcs8(); + try { + return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } finally { + Arrays.fill(encoded, (byte) 0); + } } - }, null); + }); } /** diff --git a/lib/src/main/java/zeroecho/core/alg/saber/SaberKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/saber/SaberKeyGenSpec.java index 51b15cb..ae2b067 100644 --- a/lib/src/main/java/zeroecho/core/alg/saber/SaberKeyGenSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/saber/SaberKeyGenSpec.java @@ -43,7 +43,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * A {@code SaberKeyGenSpec} selects one of the SABER parameter variants * standardized in round-3 submissions. Each variant balances performance, * bandwidth, and security level. This spec is passed to a registered - * {@link zeroecho.core.spi.AsymmetricKeyBuilder} to generate a SABER key pair. + * {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} to generate a SABER key pair. *

        * *

        Variants

        The {@link Variant} enumeration identifies supported SABER @@ -65,7 +65,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * *

        Usage example

        {@code
          * SaberKeyGenSpec spec = SaberKeyGenSpec.saberkem256r3();
        - * KeyPair kp = CryptoAlgorithms.keyPair("SABER", spec);
        + * KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("SABER", spec);
          * }
        * * @since 1.0 diff --git a/lib/src/main/java/zeroecho/core/alg/saber/SaberPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/saber/SaberPrivateKeySpec.java index 1bdd27f..6e665af 100644 --- a/lib/src/main/java/zeroecho/core/alg/saber/SaberPrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/saber/SaberPrivateKeySpec.java @@ -33,8 +33,12 @@ ******************************************************************************/ package zeroecho.core.alg.saber; +import java.util.Arrays; import java.util.Base64; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq.Cursor; @@ -50,23 +54,24 @@ import zeroecho.core.spec.AlgorithmKeySpec; * representation. *

        * - *

        Immutability

        + *

        Lifecycle

        *

        * The internal PKCS#8 encoding is defensively copied on construction and on - * retrieval via {@link #pkcs8()}. Instances are therefore immutable and - * thread-safe. + * retrieval via {@link #pkcs8()}. Access and destruction are synchronized. *

        * *

        Usage example

        {@code
          * // Import SABER private key from encoded form
          * byte[] pkcs8Bytes = ...;
          * SaberPrivateKeySpec spec = new SaberPrivateKeySpec(pkcs8Bytes);
        - * PrivateKey key = CryptoAlgorithms.privateKey("SABER", spec);
        + * PrivateKey key = session.keyBuilders().asymmetric().importPrivate("SABER", spec);
          * }
        */ -public final class SaberPrivateKeySpec implements AlgorithmKeySpec { +public final class SaberPrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] pkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Constructs a private key specification from a PKCS#8-encoded SABER private @@ -85,7 +90,13 @@ public final class SaberPrivateKeySpec implements AlgorithmKeySpec { * @return cloned byte array containing the PKCS#8 encoding */ public byte[] pkcs8() { - return pkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return pkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -103,7 +114,7 @@ public final class SaberPrivateKeySpec implements AlgorithmKeySpec { * @throws NullPointerException if {@code spec} is {@code null} */ public static PairSeq marshal(SaberPrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "SaberPrivateKeySpec", PKCS8_B64, b64); } @@ -129,7 +140,12 @@ public final class SaberPrivateKeySpec implements AlgorithmKeySpec { if (b64 == null) { throw new IllegalArgumentException("SaberPrivateKeySpec: missing pkcs8.b64"); } - return new SaberPrivateKeySpec(Base64.getDecoder().decode(b64)); + byte[] decoded = Base64.getDecoder().decode(b64); + try { + return new SaberPrivateKeySpec(decoded); + } finally { + Arrays.fill(decoded, (byte) 0); + } } /** @@ -145,4 +161,43 @@ public final class SaberPrivateKeySpec implements AlgorithmKeySpec { public String toString() { return "SaberPrivateKeySpec[len=" + pkcs8.length + "]"; } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(pkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(pkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("Saber private key specification has been destroyed"); + } + } } diff --git a/lib/src/main/java/zeroecho/core/alg/saber/SaberPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/saber/SaberPublicKeySpec.java index 2482162..03f3375 100644 --- a/lib/src/main/java/zeroecho/core/alg/saber/SaberPublicKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/saber/SaberPublicKeySpec.java @@ -61,7 +61,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * // Import SABER public key from encoded form * byte[] x509Bytes = ...; * SaberPublicKeySpec spec = new SaberPublicKeySpec(x509Bytes); - * PublicKey key = CryptoAlgorithms.publicKey("SABER", spec); + * PublicKey key = session.keyBuilders().asymmetric().importPublic("SABER", spec); * } */ public final class SaberPublicKeySpec implements AlgorithmKeySpec { diff --git a/lib/src/main/java/zeroecho/core/alg/saber/package-info.java b/lib/src/main/java/zeroecho/core/alg/saber/package-info.java index f72fdc8..81fc370 100644 --- a/lib/src/main/java/zeroecho/core/alg/saber/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/saber/package-info.java @@ -49,8 +49,9 @@ * KEM. *
      • Provide a {@link zeroecho.core.context.KemContext} bound to a public key * (encapsulation) or a private key (decapsulation).
      • - *
      • Expose immutable specifications for key generation and encoded-key - * carriers with compact marshalling helpers.
      • + *
      • Expose immutable key-generation specifications and defensively copying + * encoded-key carriers with compact marshalling helpers; private-key carriers + * are destroyable.
      • *
      • Validate presence of a suitable PQC provider before performing * operations.
      • *
      diff --git a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaAlgorithm.java index ef1dad1..ce6ba47 100644 --- a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaAlgorithm.java @@ -98,8 +98,9 @@ public final class SlhDsaAlgorithm extends AbstractCryptoAlgorithm { } }, () -> VoidSpec.INSTANCE); - registerAsymmetricKeyBuilder(SlhDsaKeyGenSpec.class, new SlhDsaKeyGenBuilder(), SlhDsaKeyGenSpec::defaultSpec); - registerAsymmetricKeyBuilder(SlhDsaPublicKeySpec.class, new SlhDsaPublicKeyBuilder(), null); - registerAsymmetricKeyBuilder(SlhDsaPrivateKeySpec.class, new SlhDsaPrivateKeyBuilder(), null); + registerAsymmetricKeyPairGenerator(SlhDsaKeyGenSpec.class, new SlhDsaKeyGenBuilder(), + SlhDsaKeyGenSpec::defaultSpec); + registerPublicKeyImporter(SlhDsaPublicKeySpec.class, new SlhDsaPublicKeyBuilder()); + registerPrivateKeyImporter(SlhDsaPrivateKeySpec.class, new SlhDsaPrivateKeyBuilder()); } } diff --git a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenBuilder.java index bacb461..fa23734 100644 --- a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaKeyGenBuilder.java @@ -40,7 +40,7 @@ import java.security.KeyPairGenerator; import java.util.Locale; import java.util.Objects; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; /** * Key pair builder for SLH-DSA (FIPS 205) using the Bouncy Castle PQC provider. @@ -54,7 +54,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * * @since 1.0 */ -public final class SlhDsaKeyGenBuilder implements AsymmetricKeyBuilder { +public final class SlhDsaKeyGenBuilder implements AsymmetricKeyPairGenerator { private static final String ALG = "SLH-DSA"; @@ -72,16 +72,6 @@ public final class SlhDsaKeyGenBuilder implements AsymmetricKeyBuilder { +public final class SlhDsaPrivateKeyBuilder implements PrivateKeyImporter { private static final String ALG = "SLH-DSA"; - @Override - public KeyPair generateKeyPair(SlhDsaPrivateKeySpec spec) { - throw new UnsupportedOperationException("Generation not supported by this spec."); - } - - @Override - public PublicKey importPublic(SlhDsaPrivateKeySpec spec) { - throw new UnsupportedOperationException("Use SlhDsaPublicKeySpec for public keys."); - } - @Override public PrivateKey importPrivate(SlhDsaPrivateKeySpec spec) throws GeneralSecurityException { KeyFactory kf = (spec.providerName() == null) ? KeyFactory.getInstance(ALG) : KeyFactory.getInstance(ALG, spec.providerName()); - return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); + byte[] encoded = spec.encoded(); + try { + return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } finally { + Arrays.fill(encoded, (byte) 0); + } } } diff --git a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPrivateKeySpec.java index f6a9b56..9a2cc64 100644 --- a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPrivateKeySpec.java @@ -33,7 +33,11 @@ ******************************************************************************/ package zeroecho.core.alg.slhdsa; +import java.util.Arrays; import java.util.Base64; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.spec.AlgorithmKeySpec; @@ -42,7 +46,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * Encoded representation of an SLH-DSA private key. * *

      - * {@code SlhDsaPrivateKeySpec} is an immutable value object that wraps a + * {@code SlhDsaPrivateKeySpec} is a destroyable value object that wraps a * PKCS#8-encoded SLH-DSA private key together with the JCA provider name that * should be used when importing the key. *

      @@ -71,15 +75,17 @@ import zeroecho.core.spec.AlgorithmKeySpec; * *

      Thread-safety

      *

      - * Instances are immutable and therefore thread-safe. + * Access and destruction are synchronized. *

      * * @since 1.0 */ -public final class SlhDsaPrivateKeySpec implements AlgorithmKeySpec { +public final class SlhDsaPrivateKeySpec implements AlgorithmKeySpec, Destroyable { private final byte[] encodedPkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); private final String providerName; + private boolean destroyed; /** * Creates a new specification using the default provider {@code "BC"}. @@ -117,7 +123,13 @@ public final class SlhDsaPrivateKeySpec implements AlgorithmKeySpec { * @return a copy of the encoded private key */ public byte[] encoded() { - return encodedPkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return encodedPkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -142,7 +154,7 @@ public final class SlhDsaPrivateKeySpec implements AlgorithmKeySpec { * @throws NullPointerException if {@code spec} is {@code null} */ public static PairSeq marshal(SlhDsaPrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "SLHDSA-PRIV", "pkcs8.b64", b64, "provider", spec.providerName); } @@ -162,20 +174,74 @@ public final class SlhDsaPrivateKeySpec implements AlgorithmKeySpec { public static SlhDsaPrivateKeySpec unmarshal(PairSeq p) { byte[] out = null; String prov = "BC"; - PairSeq.Cursor c = p.cursor(); - while (c.next()) { - String k = c.key(); - String v = c.value(); - switch (k) { - case "pkcs8.b64" -> out = Base64.getDecoder().decode(v); - case "provider" -> prov = v; - default -> { + try { + PairSeq.Cursor c = p.cursor(); + while (c.next()) { + String k = c.key(); + String v = c.value(); + switch (k) { + case "pkcs8.b64" -> out = decodeReplacing(out, v); + case "provider" -> prov = v; + default -> { + } } } + if (out == null) { + throw new IllegalArgumentException("pkcs8.b64 missing for SLH-DSA private key"); + } + return new SlhDsaPrivateKeySpec(out, prov); + } finally { + wipe(out); } - if (out == null) { - throw new IllegalArgumentException("pkcs8.b64 missing for SLH-DSA private key"); + } + + private static byte[] decodeReplacing(byte[] current, String encoded) { + wipe(current); + return Base64.getDecoder().decode(encoded); + } + + private static void wipe(byte[] current) { + if (current != null) { + Arrays.fill(current, (byte) 0); + } + } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(encodedPkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(encodedPkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("SLH-DSA private key specification has been destroyed"); } - return new SlhDsaPrivateKeySpec(out, prov); } } diff --git a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeyBuilder.java index 8d7fb9c..7619686 100644 --- a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeyBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeyBuilder.java @@ -35,36 +35,24 @@ package zeroecho.core.alg.slhdsa; import java.security.GeneralSecurityException; import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.PrivateKey; import java.security.PublicKey; import java.security.spec.X509EncodedKeySpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.PublicKeyImporter; /** * Builder for importing SLH-DSA public keys from encoded specifications. * * @since 1.0 */ -public final class SlhDsaPublicKeyBuilder implements AsymmetricKeyBuilder { +public final class SlhDsaPublicKeyBuilder implements PublicKeyImporter { private static final String ALG = "SLH-DSA"; - @Override - public KeyPair generateKeyPair(SlhDsaPublicKeySpec spec) { - throw new UnsupportedOperationException("Generation not supported by this spec."); - } - @Override public PublicKey importPublic(SlhDsaPublicKeySpec spec) throws GeneralSecurityException { KeyFactory kf = (spec.providerName() == null) ? KeyFactory.getInstance(ALG) : KeyFactory.getInstance(ALG, spec.providerName()); return kf.generatePublic(new X509EncodedKeySpec(spec.encoded())); } - - @Override - public PrivateKey importPrivate(SlhDsaPublicKeySpec spec) { - throw new UnsupportedOperationException("Use SlhDsaPrivateKeySpec for private keys."); - } } diff --git a/lib/src/main/java/zeroecho/core/alg/slhdsa/package-info.java b/lib/src/main/java/zeroecho/core/alg/slhdsa/package-info.java index f1097b8..eadcb32 100644 --- a/lib/src/main/java/zeroecho/core/alg/slhdsa/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/slhdsa/package-info.java @@ -76,7 +76,8 @@ *
    107. Encoded public and private keys are represented by * {@link zeroecho.core.alg.slhdsa.SlhDsaPublicKeySpec} and * {@link zeroecho.core.alg.slhdsa.SlhDsaPrivateKeySpec}.
    108. - *
    109. All key specifications are immutable and use defensive copies.
    110. + *
    111. All key specifications use defensive copies; private-key specifications + * are destroyable.
    112. * * *

      Streaming signature model

      diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusAlgorithm.java index 8caed45..1c4ad68 100644 --- a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusAlgorithm.java @@ -79,17 +79,17 @@ import zeroecho.core.spec.VoidSpec; * *

      Example

      {@code
        * CryptoAlgorithm alg = new SphincsPlusAlgorithm();
      - * KeyPair kp = alg.asymmetricKeyBuilder(SphincsPlusKeyGenSpec.class)
      + * KeyPair kp = alg.asymmetricKeyPairGenerator(SphincsPlusKeyGenSpec.class)
        *                 .generateKeyPair(SphincsPlusKeyGenSpec.sphincsPlusSha256_128s());
        *
        * try (SignatureContext signer =
      - *          alg.create(KeyUsage.SIGN, kp.getPrivate(), null)) {
      + *          alg.createContext(KeyUsage.SIGN, kp.getPrivate(), null)) {
        *     signer.update(message);
        *     byte[] sig = signer.sign();
        * }
        *
        * try (SignatureContext verifier =
      - *          alg.create(KeyUsage.VERIFY, kp.getPublic(), null)) {
      + *          alg.createContext(KeyUsage.VERIFY, kp.getPublic(), null)) {
        *     verifier.update(message);
        *     boolean ok = verifier.verify(sig);
        * }
      @@ -151,9 +151,9 @@ public final class SphincsPlusAlgorithm extends AbstractCryptoAlgorithm {
                           }
                       }, () -> VoidSpec.INSTANCE);
       
      -        registerAsymmetricKeyBuilder(SphincsPlusKeyGenSpec.class, new SphincsPlusKeyGenBuilder(),
      +        registerAsymmetricKeyPairGenerator(SphincsPlusKeyGenSpec.class, new SphincsPlusKeyGenBuilder(),
                       SphincsPlusKeyGenSpec::defaultSpec);
      -        registerAsymmetricKeyBuilder(SphincsPlusPublicKeySpec.class, new SphincsPlusPublicKeyBuilder(), null);
      -        registerAsymmetricKeyBuilder(SphincsPlusPrivateKeySpec.class, new SphincsPlusPrivateKeyBuilder(), null);
      +        registerPublicKeyImporter(SphincsPlusPublicKeySpec.class, new SphincsPlusPublicKeyBuilder());
      +        registerPrivateKeyImporter(SphincsPlusPrivateKeySpec.class, new SphincsPlusPrivateKeyBuilder());
           }
       }
      diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusKeyGenBuilder.java
      index 3fba3cc..8078506 100644
      --- a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusKeyGenBuilder.java
      +++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusKeyGenBuilder.java
      @@ -40,7 +40,7 @@ import java.security.KeyPairGenerator;
       import java.util.Locale;
       import java.util.Objects;
       
      -import zeroecho.core.spi.AsymmetricKeyBuilder;
      +import zeroecho.core.spi.AsymmetricKeyPairGenerator;
       
       /**
        * Key pair builder for the SPHINCS+ post-quantum signature scheme.
      @@ -54,15 +54,10 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
        * Reflection is used to avoid a hard dependency on all parameter variants.
        * 

      * - *

      Supported flows

      - *
        - *
      • {@link #generateKeyPair(SphincsPlusKeyGenSpec)}: creates a fresh key pair - * using the specified parameter set and optional provider name.
      • - *
      • {@link #importPublic(SphincsPlusKeyGenSpec)} and - * {@link #importPrivate(SphincsPlusKeyGenSpec)}: not supported; use - * {@link SphincsPlusPublicKeySpec} or {@link SphincsPlusPrivateKeySpec} - * instead.
      • - *
      + *

      The exact supported operation is + * {@link #generateKeyPair(SphincsPlusKeyGenSpec)}. Public and private import are + * registered separately for {@link SphincsPlusPublicKeySpec} and + * {@link SphincsPlusPrivateKeySpec}.

      * *

      Example

      {@code
        * SphincsPlusKeyGenSpec spec =
      @@ -73,7 +68,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
        *
        * @since 1.0
        */
      -public final class SphincsPlusKeyGenBuilder implements AsymmetricKeyBuilder {
      +public final class SphincsPlusKeyGenBuilder implements AsymmetricKeyPairGenerator {
       
           private static final String ALG = "SPHINCSPlus";
       
      @@ -108,40 +103,6 @@ public final class SphincsPlusKeyGenBuilder implements AsymmetricKeyBuilder
      -     * Public key import is delegated to {@link SphincsPlusPublicKeySpec} and its
      -     * associated builder.
      -     * 

      - * - * @param spec ignored - * @return never returns normally - * @throws UnsupportedOperationException always - */ - @Override - public java.security.PublicKey importPublic(SphincsPlusKeyGenSpec spec) { - throw new UnsupportedOperationException("Use SphincsPlusPublicKeySpec to import a public key."); - } - - /** - * Not supported for this builder. - * - *

      - * Private key import is delegated to {@link SphincsPlusPrivateKeySpec} and its - * associated builder. - *

      - * - * @param spec ignored - * @return never returns normally - * @throws UnsupportedOperationException always - */ - @Override - public java.security.PrivateKey importPrivate(SphincsPlusKeyGenSpec spec) { - throw new UnsupportedOperationException("Use SphincsPlusPrivateKeySpec to import a private key."); - } - /** * Resolves the Bouncy Castle parameter spec constant that corresponds to the * given high-level {@link SphincsPlusKeyGenSpec}. diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeyBuilder.java index cb3f288..d93cb1d 100644 --- a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeyBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeyBuilder.java @@ -35,12 +35,11 @@ package zeroecho.core.alg.sphincsplus; import java.security.GeneralSecurityException; import java.security.KeyFactory; -import java.security.KeyPair; import java.security.PrivateKey; -import java.security.PublicKey; import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.PrivateKeyImporter; /** * Builder for importing SPHINCS+ private keys from encoded specifications. @@ -52,14 +51,9 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * pairs but focuses solely on importing private key material. *

      * - *

      Supported flows

      - *
        - *
      • {@link #importPrivate(SphincsPlusPrivateKeySpec)}: imports a SPHINCS+ - * private key from its encoded PKCS#8 representation.
      • - *
      • {@link #generateKeyPair(SphincsPlusPrivateKeySpec)}: not supported.
      • - *
      • {@link #importPublic(SphincsPlusPrivateKeySpec)}: not supported; use - * {@link SphincsPlusPublicKeySpec} and its builder instead.
      • - *
      + *

      The exact supported operation is + * {@link #importPrivate(SphincsPlusPrivateKeySpec)}. Other key operations are + * registered through their own exact interfaces.

      * *

      Example

      {@code
        * // Assuming bytes contain a PKCS#8-encoded SPHINCS+ private key:
      @@ -72,40 +66,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
        *
        * @since 1.0
        */
      -public final class SphincsPlusPrivateKeyBuilder implements AsymmetricKeyBuilder {
      -    /**
      -     * Not supported for this builder.
      -     *
      -     * 

      - * Key pair generation requires a parameter set and is handled by - * {@link SphincsPlusKeyGenBuilder}. This method always throws. - *

      - * - * @param spec ignored - * @return never returns normally - * @throws UnsupportedOperationException always - */ - @Override - public KeyPair generateKeyPair(SphincsPlusPrivateKeySpec spec) { - throw new UnsupportedOperationException("Generation not supported by this spec."); - } - - /** - * Not supported for this builder. - * - *

      - * Public key import should be performed via {@link SphincsPlusPublicKeySpec} - * and its associated builder. - *

      - * - * @param spec ignored - * @return never returns normally - * @throws UnsupportedOperationException always - */ - @Override - public PublicKey importPublic(SphincsPlusPrivateKeySpec spec) { - throw new UnsupportedOperationException("Use SphincsPlusPublicKeySpec for public keys."); - } +public final class SphincsPlusPrivateKeyBuilder implements PrivateKeyImporter { /** * Imports a SPHINCS+ private key from PKCS#8 encoding. @@ -128,6 +89,11 @@ public final class SphincsPlusPrivateKeyBuilder implements AsymmetricKeyBuilder< public PrivateKey importPrivate(SphincsPlusPrivateKeySpec spec) throws GeneralSecurityException { KeyFactory kf = (spec.providerName() == null) ? KeyFactory.getInstance("SPHINCSPlus") : KeyFactory.getInstance("SPHINCSPlus", spec.providerName()); - return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); + byte[] encoded = spec.encoded(); + try { + return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } finally { + Arrays.fill(encoded, (byte) 0); + } } } diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeySpec.java index 86f3e29..d45f4a5 100644 --- a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeySpec.java @@ -33,7 +33,11 @@ ******************************************************************************/ package zeroecho.core.alg.sphincsplus; +import java.util.Arrays; import java.util.Base64; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.spec.AlgorithmKeySpec; @@ -44,7 +48,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; *

      * {@code SphincsPlusPrivateKeySpec} wraps a PKCS#8-encoded SPHINCS+ private key * along with the provider name that should be used for import. It is a simple - * immutable holder designed for use with {@link SphincsPlusPrivateKeyBuilder}. + * destroyable holder designed for use with {@link SphincsPlusPrivateKeyBuilder}. *

      * *

      Encoding

      @@ -57,8 +61,8 @@ import zeroecho.core.spec.AlgorithmKeySpec; * provider) if not explicitly supplied. * * - *

      Thread-safety

      Instances are immutable and can be shared safely - * across threads. Defensive copies are returned for all sensitive material. + *

      Thread-safety

      Access and destruction are synchronized. Defensive + * copies are returned for all sensitive material. * *

      Example

      {@code
        * // Wrap a PKCS#8-encoded private key
      @@ -77,9 +81,11 @@ import zeroecho.core.spec.AlgorithmKeySpec;
        *
        * @since 1.0
        */
      -public final class SphincsPlusPrivateKeySpec implements AlgorithmKeySpec {
      +public final class SphincsPlusPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
           private final byte[] encodedPkcs8;
      +    private final ReentrantLock lifecycleLock = new ReentrantLock();
           private final String providerName; // e.g. "BCPQC"
      +    private boolean destroyed;
       
           /**
            * Constructs a new specification with the default provider {@code "BCPQC"}.
      @@ -113,7 +119,13 @@ public final class SphincsPlusPrivateKeySpec implements AlgorithmKeySpec {
            * @return clone of the encoded key bytes
            */
           public byte[] encoded() {
      -        return encodedPkcs8.clone();
      +        lifecycleLock.lock();
      +        try {
      +            ensureActive();
      +            return encodedPkcs8.clone();
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
           }
       
           /**
      @@ -137,7 +149,7 @@ public final class SphincsPlusPrivateKeySpec implements AlgorithmKeySpec {
            * @return serialized {@link PairSeq} representation
            */
           public static PairSeq marshal(SphincsPlusPrivateKeySpec spec) {
      -        String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8);
      +        String b64 = spec.encodedKey();
               return PairSeq.of("type", "SPHINCSPLUS-PRIV", "pkcs8.b64", b64, "provider", spec.providerName);
           }
       
      @@ -156,20 +168,74 @@ public final class SphincsPlusPrivateKeySpec implements AlgorithmKeySpec {
           public static SphincsPlusPrivateKeySpec unmarshal(PairSeq p) {
               byte[] out = null;
               String prov = "BCPQC";
      -        PairSeq.Cursor c = p.cursor();
      -        while (c.next()) {
      -            String k = c.key();
      -            String v = c.value();
      -            switch (k) {
      -                case "pkcs8.b64" -> out = Base64.getDecoder().decode(v);
      -                case "provider" -> prov = v;
      -                default -> {
      +        try {
      +            PairSeq.Cursor c = p.cursor();
      +            while (c.next()) {
      +                String k = c.key();
      +                String v = c.value();
      +                switch (k) {
      +                    case "pkcs8.b64" -> out = decodeReplacing(out, v);
      +                    case "provider" -> prov = v;
      +                    default -> {
      +                    }
                       }
                   }
      +            if (out == null) {
      +                throw new IllegalArgumentException("pkcs8.b64 missing for SPHINCS+ private key");
      +            }
      +            return new SphincsPlusPrivateKeySpec(out, prov);
      +        } finally {
      +            wipe(out);
               }
      -        if (out == null) {
      -            throw new IllegalArgumentException("pkcs8.b64 missing for SPHINCS+ private key");
      +    }
      +
      +    private static byte[] decodeReplacing(byte[] current, String encoded) {
      +        wipe(current);
      +        return Base64.getDecoder().decode(encoded);
      +    }
      +
      +    private static void wipe(byte[] current) {
      +        if (current != null) {
      +            Arrays.fill(current, (byte) 0);
      +        }
      +    }
      +
      +    private String encodedKey() {
      +        lifecycleLock.lock();
      +        try {
      +            ensureActive();
      +            return Base64.getEncoder().withoutPadding().encodeToString(encodedPkcs8);
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
      +    }
      +
      +    @Override
      +    public void destroy() {
      +        lifecycleLock.lock();
      +        try {
      +            if (!destroyed) {
      +                Arrays.fill(encodedPkcs8, (byte) 0);
      +                destroyed = true;
      +            }
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
      +    }
      +
      +    @Override
      +    public boolean isDestroyed() {
      +        lifecycleLock.lock();
      +        try {
      +            return destroyed;
      +        } finally {
      +            lifecycleLock.unlock();
      +        }
      +    }
      +
      +    private void ensureActive() {
      +        if (destroyed) {
      +            throw new IllegalStateException("SPHINCS+ private key specification has been destroyed");
               }
      -        return new SphincsPlusPrivateKeySpec(out, prov);
           }
       }
      diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPublicKeyBuilder.java
      index d1dade2..5eef249 100644
      --- a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPublicKeyBuilder.java
      +++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPublicKeyBuilder.java
      @@ -35,12 +35,10 @@ package zeroecho.core.alg.sphincsplus;
       
       import java.security.GeneralSecurityException;
       import java.security.KeyFactory;
      -import java.security.KeyPair;
      -import java.security.PrivateKey;
       import java.security.PublicKey;
       import java.security.spec.X509EncodedKeySpec;
       
      -import zeroecho.core.spi.AsymmetricKeyBuilder;
      +import zeroecho.core.spi.PublicKeyImporter;
       
       /**
        * Builder for importing SPHINCS+ public keys from encoded specifications.
      @@ -52,14 +50,9 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
        * pairs, but focuses solely on importing public key material.
        * 

      * - *

      Supported flows

      - *
        - *
      • {@link #importPublic(SphincsPlusPublicKeySpec)}: imports a SPHINCS+ - * public key from its encoded X.509 representation.
      • - *
      • {@link #generateKeyPair(SphincsPlusPublicKeySpec)}: not supported.
      • - *
      • {@link #importPrivate(SphincsPlusPublicKeySpec)}: not supported; use - * {@link SphincsPlusPrivateKeySpec} and its builder instead.
      • - *
      + *

      The exact supported operation is + * {@link #importPublic(SphincsPlusPublicKeySpec)}. Other key operations are + * registered through their own exact interfaces.

      * *

      Example

      {@code
        * // Assuming bytes contain an X.509-encoded SPHINCS+ public key:
      @@ -72,24 +65,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
        *
        * @since 1.0
        */
      -public final class SphincsPlusPublicKeyBuilder implements AsymmetricKeyBuilder {
      -
      -    /**
      -     * Not supported for this builder.
      -     *
      -     * 

      - * Key pair generation requires algorithm parameters and is handled by - * {@link SphincsPlusKeyGenBuilder}. This method always throws. - *

      - * - * @param spec ignored - * @return never returns normally - * @throws UnsupportedOperationException always - */ - @Override - public KeyPair generateKeyPair(SphincsPlusPublicKeySpec spec) { - throw new UnsupportedOperationException("Generation not supported by this spec."); - } +public final class SphincsPlusPublicKeyBuilder implements PublicKeyImporter { /** * Imports a SPHINCS+ public key from X.509 encoding. @@ -114,21 +90,4 @@ public final class SphincsPlusPublicKeyBuilder implements AsymmetricKeyBuilder - * Private key import should be performed via {@link SphincsPlusPrivateKeySpec} - * and its associated builder. - *

      - * - * @param spec ignored - * @return never returns normally - * @throws UnsupportedOperationException always - */ - @Override - public PrivateKey importPrivate(SphincsPlusPublicKeySpec spec) { - throw new UnsupportedOperationException("Use SphincsPlusPrivateKeySpec for private keys."); - } } diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/package-info.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/package-info.java index cac1140..54c89de 100644 --- a/lib/src/main/java/zeroecho/core/alg/sphincsplus/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/package-info.java @@ -51,8 +51,9 @@ * the key's parameter set. *
    113. Provide key builders for generating new key pairs and for importing * encoded public and private keys.
    114. - *
    115. Expose immutable key specification types that defensively copy sensitive - * material and support compact marshalling.
    116. + *
    117. Expose key specification types that defensively copy sensitive material + * and support compact marshalling; private-key specifications are + * destroyable.
    118. * * *

      Components

      @@ -67,8 +68,8 @@ *
    119. SphincsPlusPublicKeyBuilder / SphincsPlusPrivateKeyBuilder: * importers backed by JCA key factories.
    120. *
    121. SphincsPlusPublicKeySpec / SphincsPlusPrivateKeySpec: - * immutable wrappers over X.509 and PKCS#8 encodings with marshalling - * helpers.
    122. + * wrappers over X.509 and PKCS#8 encodings with marshalling helpers; the + * private-key form is destroyable. * * *

      Design notes

      diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/XdhAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/xdh/XdhAlgorithm.java index 20b8c80..7d120f9 100644 --- a/lib/src/main/java/zeroecho/core/alg/xdh/XdhAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/xdh/XdhAlgorithm.java @@ -35,11 +35,11 @@ package zeroecho.core.alg.xdh; import java.security.GeneralSecurityException; import java.security.KeyFactory; -import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; import zeroecho.core.AlgorithmFamily; import zeroecho.core.KeyUsage; @@ -49,7 +49,8 @@ import zeroecho.core.alg.common.agreement.GenericJcaMessageAgreementContext; import zeroecho.core.alg.common.agreement.KeyPairKey; import zeroecho.core.context.AgreementContext; import zeroecho.core.context.MessageAgreementContext; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; /** * Algorithm definition for XDH (elliptic curve Diffie-Hellman) key agreement, @@ -97,16 +98,16 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * CryptoAlgorithm xdh = new XdhAlgorithm(); * * // Generate key pairs - * KeyPair a = xdh.asymmetricKeyBuilder(XdhSpec.class).generateKeyPair(XdhSpec.X25519); - * KeyPair b = xdh.asymmetricKeyBuilder(XdhSpec.class).generateKeyPair(XdhSpec.X25519); + * KeyPair a = xdh.asymmetricKeyPairGenerator(XdhSpec.class).generateKeyPair(XdhSpec.X25519); + * KeyPair b = xdh.asymmetricKeyPairGenerator(XdhSpec.class).generateKeyPair(XdhSpec.X25519); * * // Perform agreement on side A - * AgreementContext ctxA = xdh.create(KeyUsage.AGREEMENT, a.getPrivate(), XdhSpec.X25519); + * AgreementContext ctxA = xdh.createContext(KeyUsage.AGREEMENT, a.getPrivate(), XdhSpec.X25519); * ctxA.setPeerPublic(b.getPublic()); * byte[] secretA = ctxA.deriveSecret(); * * // Perform agreement on side B - * AgreementContext ctxB = xdh.create(KeyUsage.AGREEMENT, b.getPrivate(), XdhSpec.X25519); + * AgreementContext ctxB = xdh.createContext(KeyUsage.AGREEMENT, b.getPrivate(), XdhSpec.X25519); * ctxB.setPeerPublic(a.getPublic()); * byte[] secretB = ctxB.deriveSecret(); * @@ -153,42 +154,27 @@ public final class XdhAlgorithm extends AbstractCryptoAlgorithm { s.keyAgreementName(), null, "XDH", null), () -> XdhSpec.X25519); - registerAsymmetricKeyBuilder(XdhSpec.class, new XdhKeyGenBuilder(), () -> XdhSpec.X25519); - registerAsymmetricKeyBuilder(XdhPublicKeySpec.class, new AsymmetricKeyBuilder<>() { - - @Override - public KeyPair generateKeyPair(XdhPublicKeySpec spec) throws GeneralSecurityException { - throw new UnsupportedOperationException("Use XdhKeyGenBuilder for keypair generation."); - } + registerAsymmetricKeyPairGenerator(XdhSpec.class, new XdhKeyGenBuilder(), () -> XdhSpec.X25519); + registerPublicKeyImporter(XdhPublicKeySpec.class, new PublicKeyImporter<>() { @Override public PublicKey importPublic(XdhPublicKeySpec spec) throws GeneralSecurityException { KeyFactory kf = KeyFactory.getInstance("XDH"); return kf.generatePublic(new X509EncodedKeySpec(spec.encoded())); } - - @Override - public PrivateKey importPrivate(XdhPublicKeySpec spec) throws GeneralSecurityException { - throw new UnsupportedOperationException("Use XdhPrivateKeySpec for private key import."); - } - }, null); - registerAsymmetricKeyBuilder(XdhPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { - - @Override - public KeyPair generateKeyPair(XdhPrivateKeySpec spec) throws GeneralSecurityException { - throw new UnsupportedOperationException("Use XdhKeyGenBuilder for keypair generation."); - } - - @Override - public PublicKey importPublic(XdhPrivateKeySpec spec) throws GeneralSecurityException { - throw new UnsupportedOperationException("Use XdhPrivateKeySpec for public key import."); - } + }); + registerPrivateKeyImporter(XdhPrivateKeySpec.class, new PrivateKeyImporter<>() { @Override public PrivateKey importPrivate(XdhPrivateKeySpec spec) throws GeneralSecurityException { KeyFactory kf = KeyFactory.getInstance("XDH"); - return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); + byte[] encoded = spec.encoded(); + try { + return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } finally { + Arrays.fill(encoded, (byte) 0); + } } - }, null); + }); } } diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/XdhKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/xdh/XdhKeyGenBuilder.java index 2b19e77..227bfb4 100644 --- a/lib/src/main/java/zeroecho/core/alg/xdh/XdhKeyGenBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/xdh/XdhKeyGenBuilder.java @@ -37,7 +37,7 @@ import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyPairGenerator; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; /** * KeyPair generator for XDH curves using the JCA KeyPairGenerator SPI. @@ -50,9 +50,8 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * *

      Design and scope

      *
        - *
      • Generation only: This builder supports key generation. - * Public/private import is intentionally unsupported and will throw - * {@link UnsupportedOperationException}.
      • + *
      • Generation only: This implementation exposes only the exact + * key-pair generation capability; import operations are registered separately.
      • *
      • Provider resolution: The default JCA provider selection is used. * If a specific provider is required, supply or register one that exposes the * requested XDH algorithm name.
      • @@ -69,7 +68,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * * @since 1.0 */ -public final class XdhKeyGenBuilder implements AsymmetricKeyBuilder { +public final class XdhKeyGenBuilder implements AsymmetricKeyPairGenerator { /** * Generates a new XDH key pair using the JCA * {@link java.security.KeyPairGenerator}. @@ -95,42 +94,4 @@ public final class XdhKeyGenBuilder implements AsymmetricKeyBuilder { KeyPairGenerator kpg = KeyPairGenerator.getInstance(spec.kpgName()); return kpg.generateKeyPair(); } - - /** - * Not supported: importing XDH public keys is outside the scope of this - * builder. - * - *

        - * Use a dedicated import builder or provider-specific utilities if you need to - * wrap encoded public keys. - *

        - * - * @param spec the XDH key specification - * @return never returns normally - * @throws UnsupportedOperationException always thrown to indicate unsupported - * operation - */ - @Override - public java.security.PublicKey importPublic(XdhSpec spec) { - throw new UnsupportedOperationException(); - } - - /** - * Not supported: importing XDH private keys is outside the scope of this - * builder. - * - *

        - * Use a dedicated import builder or provider-specific utilities if you need to - * wrap encoded private keys. - *

        - * - * @param spec the XDH key specification - * @return never returns normally - * @throws UnsupportedOperationException always thrown to indicate unsupported - * operation - */ - @Override - public java.security.PrivateKey importPrivate(XdhSpec spec) { - throw new UnsupportedOperationException(); - } } diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/XdhPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/xdh/XdhPrivateKeySpec.java index a166676..f570f6b 100644 --- a/lib/src/main/java/zeroecho/core/alg/xdh/XdhPrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/xdh/XdhPrivateKeySpec.java @@ -33,7 +33,11 @@ ******************************************************************************/ package zeroecho.core.alg.xdh; +import java.util.Arrays; import java.util.Base64; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.spec.AlgorithmKeySpec; @@ -75,9 +79,11 @@ import zeroecho.core.spec.AlgorithmKeySpec; * * @since 1.0 */ -public class XdhPrivateKeySpec implements AlgorithmKeySpec { +public class XdhPrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] pkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Constructs a new specification from the given PKCS#8-encoded private key. @@ -98,7 +104,13 @@ public class XdhPrivateKeySpec implements AlgorithmKeySpec { * @return a clone of the internal key encoding */ public byte[] encoded() { - return pkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return pkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -117,7 +129,7 @@ public class XdhPrivateKeySpec implements AlgorithmKeySpec { * @throws NullPointerException if {@code spec} is {@code null} */ public static PairSeq marshal(XdhPrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "XDH-PRIV", PKCS8_B64, b64); } @@ -141,12 +153,62 @@ public class XdhPrivateKeySpec implements AlgorithmKeySpec { String k = cur.key(); String v = cur.value(); if (PKCS8_B64.equals(k)) { - out = Base64.getDecoder().decode(v); + out = decodeReplacing(out, v); } } if (out == null) { throw new IllegalArgumentException("pkcs8.b64 missing for DH private key"); } - return new XdhPrivateKeySpec(out); + try { + return new XdhPrivateKeySpec(out); + } finally { + Arrays.fill(out, (byte) 0); + } + } + + private static byte[] decodeReplacing(byte[] current, String encoded) { + if (current != null) { + Arrays.fill(current, (byte) 0); + } + return Base64.getDecoder().decode(encoded); + } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(pkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(pkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("XDH private key specification has been destroyed"); + } } } diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/XdhSpec.java b/lib/src/main/java/zeroecho/core/alg/xdh/XdhSpec.java index 2182df1..a0ab79b 100644 --- a/lib/src/main/java/zeroecho/core/alg/xdh/XdhSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/xdh/XdhSpec.java @@ -50,10 +50,10 @@ import zeroecho.core.spec.ContextSpec; * *

        Usage

        {@code
          * // Generate a key pair for X25519
        - * KeyPair kp = CryptoAlgorithms.keyPair("XDH", XdhSpec.X25519);
        + * KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("XDH", XdhSpec.X25519);
          *
          * // Perform key agreement
        - * AgreementContext ctx = CryptoAlgorithms.create("XDH", KeyUsage.AGREEMENT,
        + * AgreementContext ctx = session.createContext("XDH", KeyUsage.AGREEMENT,
          *                                                kp.getPrivate(), XdhSpec.X25519);
          * ctx.setPeerPublic(peerPublicKey);
          * byte[] sharedSecret = ctx.deriveSecret();
        diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/package-info.java b/lib/src/main/java/zeroecho/core/alg/xdh/package-info.java
        index e1e4eb5..801ddb7 100644
        --- a/lib/src/main/java/zeroecho/core/alg/xdh/package-info.java
        +++ b/lib/src/main/java/zeroecho/core/alg/xdh/package-info.java
        @@ -50,8 +50,8 @@
          * suitable for KDF input.
          * 
      • Provide key builders for generating key pairs and importing encoded * public/private keys.
      • - *
      • Offer immutable key specifications that defensively copy encoded material - * and support compact marshalling.
      • + *
      • Offer key specifications that defensively copy encoded material and + * support compact marshalling; private-key specifications are destroyable.
      • *
      * *

      Components

      diff --git a/lib/src/main/java/zeroecho/core/audit/AuditListener.java b/lib/src/main/java/zeroecho/core/audit/AuditListener.java index 4af37bb..1de88a1 100644 --- a/lib/src/main/java/zeroecho/core/audit/AuditListener.java +++ b/lib/src/main/java/zeroecho/core/audit/AuditListener.java @@ -38,9 +38,7 @@ import java.security.KeyPair; import java.util.Map; import zeroecho.core.KeyUsage; -import zeroecho.core.context.CryptoContext; import zeroecho.core.spec.AlgorithmKeySpec; -import zeroecho.core.spec.ContextSpec; /** * Listener for structured audit events emitted by audited crypto contexts. @@ -72,33 +70,6 @@ import zeroecho.core.spec.ContextSpec; *

      */ public interface AuditListener { - /** - * Emitted right after a context has been created and wrapped. - * - *

      - * The callback conveys basic provenance such as provider label, the intended - * key usage role, and optional key and specification objects. Implementations - * must not attempt to extract secrets from the provided objects. - *

      - * - * @param the concrete context specification type - * @param the concrete key type - * @param id an algorithm or implementation identifier supplied by the - * creator; never null but may be a generic label such as - * "unknown" - * @param provider a provider or vendor label; may be "unknown" - * @param role the usage role associated with the context, for example - * ENCRYPTION or VERIFY - * @param key the key associated with the context if available, or null if - * not applicable - * @param spec the context specification if available, or null if not - * applicable - */ - default void onContextCreated(String id, String provider, KeyUsage role, - K key, S spec) { - // empty - } - /** * Emitted after key pair generation via SPI. * @@ -138,25 +109,6 @@ public interface AuditListener { // empty } - /** - * Emitted when a context is closed (generic form). - * - *

      - * This form mirrors legacy summary notifications and may be emitted in addition - * to the id-based closure callback. - *

      - * - * @param id an algorithm or implementation identifier supplied at - * creation time - * @param provider a provider or vendor label - * @param role the usage role associated with the context - * @param key the primary key associated with the context, or null if - * unavailable - */ - default void onContextClosed(String id, String provider, KeyUsage role, Key key) { - // empty - } - /** * Emitted when a key was destroyed. * @@ -410,21 +362,6 @@ public interface AuditListener { // empty } - /** - * Legacy single-argument creation callback. - * - *

      - * Emitted when a context is wrapped. Prefer - * {@link #onContextCreatedMeta(String, String, String, KeyUsage, String, Map)} - * for structured metadata and correlation. - *

      - * - * @param ctx the wrapped crypto context; never null - */ - default void onContextCreated(CryptoContext ctx) { - // empty - } - /** * Legacy cumulative byte counter for any role. * diff --git a/lib/src/main/java/zeroecho/core/audit/AuditListeners.java b/lib/src/main/java/zeroecho/core/audit/AuditListeners.java new file mode 100644 index 0000000..7edf79c --- /dev/null +++ b/lib/src/main/java/zeroecho/core/audit/AuditListeners.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the conditions in the project LICENSE are met. + ******************************************************************************/ +package zeroecho.core.audit; + +import java.lang.reflect.Proxy; +import java.util.Objects; + +/** + * Utilities for enforcing the best-effort audit-listener contract. + * + *

      The returned listener suppresses listener failures without logging callback + * arguments, because those arguments may refer to sensitive cryptographic + * objects. Cryptographic operation outcomes therefore never depend on an audit + * sink's availability.

      + * + * @since 1.0 + */ +public final class AuditListeners { + private AuditListeners() { + // utility class + } + + /** + * Returns a listener facade that cannot interrupt the caller. + * + * @param listener listener to protect + * @return non-throwing listener facade + * @throws NullPointerException if {@code listener} is {@code null} + */ + public static AuditListener bestEffort(AuditListener listener) { + AuditListener target = Objects.requireNonNull(listener, "listener"); + ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); + ClassLoader loader = contextLoader == null ? ClassLoader.getSystemClassLoader() : contextLoader; + return (AuditListener) Proxy.newProxyInstance(loader, + new Class[] { AuditListener.class }, (proxy, method, arguments) -> { + if (method.getDeclaringClass() == Object.class) { + return method.invoke(target, arguments); + } + try { + return method.invoke(target, arguments); + } catch (ReflectiveOperationException ignored) { + return null; + } + }); + } +} diff --git a/lib/src/main/java/zeroecho/core/audit/AuditMode.java b/lib/src/main/java/zeroecho/core/audit/AuditMode.java new file mode 100644 index 0000000..fc374a7 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/audit/AuditMode.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the conditions in the project LICENSE are met. + ******************************************************************************/ +package zeroecho.core.audit; + +/** + * Defines the session-owned automatic auditing strategy. + * + *

      Audit listener failures are best-effort diagnostics and never change the + * outcome of a cryptographic operation.

      + * + * @since 1.0 + */ +public enum AuditMode { + /** + * Emits one context-creation event and returns the original context. + */ + OFF, + /** + * Emits one context-creation event and returns an operation-auditing wrapper. + */ + WRAP, + /** + * Emits no automatic audit events and returns the original context. + */ + MANUAL +} diff --git a/lib/src/main/java/zeroecho/core/audit/AuditedContexts.java b/lib/src/main/java/zeroecho/core/audit/AuditedContexts.java index 754563a..14b2ae5 100644 --- a/lib/src/main/java/zeroecho/core/audit/AuditedContexts.java +++ b/lib/src/main/java/zeroecho/core/audit/AuditedContexts.java @@ -37,6 +37,9 @@ import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -44,6 +47,7 @@ import java.security.Key; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; +import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Map; import java.util.Objects; @@ -101,8 +105,8 @@ import zeroecho.core.spec.ContextSpec; *
    123. Counting is performed by decorating the returned {@code InputStream}s; no * buffering beyond normal {@code FilterInputStream} forwarding is * introduced.
    124. - *
    125. Idempotent wrapping: contexts that are already JDK proxies will be - * returned unchanged.
    126. + *
    127. Idempotent wrapping: contexts already wrapped by this utility are returned + * unchanged. Unrelated JDK proxies are wrapped normally.
    128. * * *

      Usage example

      {@code
      @@ -130,9 +134,9 @@ public final class AuditedContexts {
            * and error events while preserving the original behavior.
            *
            * 

      - * If {@code ctx} is {@code null}, this method returns {@code null}. If - * {@code ctx} is already a JDK dynamic proxy, the instance is returned as-is to - * keep wrapping idempotent. + * If {@code ctx} is {@code null}, this method returns {@code null}. A context + * already backed by this utility's auditing handler is returned as-is to keep + * wrapping idempotent; unrelated JDK proxies are wrapped normally. *

      * *

      @@ -149,8 +153,9 @@ public final class AuditedContexts { * with non-reversible key fingerprints and derived sizes. * * Creation metadata includes a generated correlation id, algorithm id, provider - * label, key fingerprint (if extractable), and a best-effort, non-secret spec - * summary when available. + * label, a public-key fingerprint or safe key type label, and a best-effort, + * non-secret spec summary when available. Private and secret key encodings are + * never requested. * *

      * This call does not invoke any context operations other than the minimal, @@ -170,23 +175,29 @@ public final class AuditedContexts { * @param role the usage role associated with the context (for example, * ENCRYPTION, DECRYPTION, SIGNING); must not be {@code null} * @return the auditing proxy for {@code ctx}, the original {@code ctx} if it is - * already a proxy, or {@code null} if {@code ctx} is {@code null} + * already wrapped by this utility, or {@code null} if {@code ctx} is + * {@code null} */ public static CryptoContext wrap(final CryptoContext ctx, final AuditListener audit, final KeyUsage role) { if (ctx == null) { return null; } - if (Proxy.isProxyClass(ctx.getClass())) { - return ctx; // idempotent + if (isAuditedProxy(ctx)) { + return ctx; } // IMPORTANT: do not call any other wrap(...) here — avoid recursion. return replaceWithProxy(ctx, audit, role); } + private static boolean isAuditedProxy(CryptoContext context) { + return Proxy.isProxyClass(context.getClass()) + && Proxy.getInvocationHandler(context) instanceof AuditingHandler; + } + @SuppressWarnings("unchecked") - private static T replaceWithProxy(final T ctx, final AuditListener audit, final KeyUsage role) { // NOPMD + private static T replaceWithProxy(final T ctx, final AuditListener audit, final KeyUsage role) { Objects.requireNonNull(ctx, "ctx must not be null"); - Objects.requireNonNull(audit, "audit must not be null"); + AuditListener safeAudit = AuditListeners.bestEffort(Objects.requireNonNull(audit, "audit must not be null")); Objects.requireNonNull(role, "role must not be null"); // Emit creation metadata if we can resolve it @@ -195,71 +206,49 @@ public final class AuditedContexts { String provider = null; String keyFp; Map specMeta = null; - Key keyObj = null; - ContextSpec specObj = null; + AccessorSet accessors = new AccessorSet(ctx); if (ctx instanceof CryptoContext cctx) { // NOPMD try { CryptoAlgorithm alg = cctx.algorithm(); if (alg != null) { try { - algoId = safeString(invokeNoArg(alg, "id")); + algoId = safeString(alg.id()); } catch (Throwable ignore) { // NOPMD algoId = alg.getClass().getSimpleName(); } try { - provider = safeString(invokeNoArg(alg, "providerLabel")); + provider = safeString(alg.providerName()); } catch (Throwable ignore) { // NOPMD - try { - provider = safeString(invokeNoArg(alg, "provider")); - } catch (Throwable ignoredToo) { // NOPMD - provider = alg.getClass().getPackageName(); - } + provider = alg.getClass().getPackageName(); } } } catch (Throwable ignore) { // NOPMD // best-effort } try { - keyObj = cctx.key(); - keyFp = fingerprint(keyObj); + keyFp = fingerprint(cctx.key()); } catch (Throwable ignore) { // NOPMD keyFp = "n/a"; } try { // optional: contexts may expose a spec() accessor - Object s = invokeNoArg(cctx, "spec"); - if (s instanceof ContextSpec) { - specObj = (ContextSpec) s; - specMeta = SpecIntrospector.summarize(specObj); + Object s = accessors.spec(); + if (s instanceof ContextSpec contextSpec) { + specMeta = SpecIntrospector.summarize(contextSpec); } } catch (Throwable ignore) { // NOPMD - specObj = null; specMeta = null; } - // New-style context-created event with metadata - audit.onContextCreatedMeta(ctxId, algoId == null ? UNKNOWN : algoId, provider == null ? UNKNOWN : provider, + safeAudit.onContextCreatedMeta(ctxId, algoId == null ? UNKNOWN : algoId, + provider == null ? UNKNOWN : provider, role, keyFp, specMeta); - - // Back-compat event forms - try { - // Generic with id/provider/role/key/spec - audit.onContextCreated(algoId == null ? UNKNOWN : algoId, provider == null ? UNKNOWN : provider, role, - keyObj, specObj); - } catch (Throwable ignore) { // NOPMD - } - try { - // Legacy single-arg callback - audit.onContextCreated(cctx); - } catch (Throwable ignore) { // NOPMD - } } ClassLoader cl = ctx.getClass().getClassLoader(); // NOPMD Class[] ifaces = allInterfaces(ctx.getClass()); - InvocationHandler handler = new AuditingHandler(ctx, audit, role, ctxId, algoId == null ? UNKNOWN : algoId, - provider == null ? UNKNOWN : provider, keyObj); + InvocationHandler handler = new AuditingHandler(ctx, safeAudit, role, ctxId, accessors); return (T) Proxy.newProxyInstance(cl, ifaces, handler); } @@ -281,9 +270,7 @@ public final class AuditedContexts { private final KeyUsage role; private final String ctxId; - private final String algoId; - private final String provider; - private final Key keyForClose; // may be null + private final AccessorSet accessors; private long bodyBytes; private long trailerBytes; @@ -294,15 +281,13 @@ public final class AuditedContexts { private String policyLabel = "UNSET"; private String expectedSource = "provided"; // default for setExpectedTag(byte[]) - private AuditingHandler(Object target, AuditListener audit, KeyUsage role, String ctxId, String algoId, - String provider, Key keyForClose) { + private AuditingHandler(Object target, AuditListener audit, KeyUsage role, String ctxId, + AccessorSet accessors) { this.target = target; this.audit = audit; this.role = role; this.ctxId = ctxId; - this.algoId = algoId; - this.provider = provider; - this.keyForClose = keyForClose; + this.accessors = accessors; this.startNanos = System.nanoTime(); } @@ -312,7 +297,7 @@ public final class AuditedContexts { // Object methods if ("toString".equals(name) && (args == null || args.length == 0)) { - return "AuditedProxy(" + target + ")"; + return "AuditedCryptoContext[type=" + target.getClass().getName() + ", role=" + role + "]"; } if ("hashCode".equals(name) && (args == null || args.length == 0)) { return System.identityHashCode(proxy); @@ -326,11 +311,12 @@ public final class AuditedContexts { // Track tagLength() if requested explicitly if ("tagLength".equals(name) && (args == null || args.length == 0)) { - Object res = method.invoke(target, args); - if (res instanceof Integer) { - this.tagLen = (Integer) res; + Integer resolvedTagLength = accessors.tagLength(); + if (resolvedTagLength != null) { + this.tagLen = resolvedTagLength; + return resolvedTagLength; } - return res; + return method.invoke(target, args); } // Track verification policy (label only; do not depend on specific enum type) @@ -344,6 +330,7 @@ public final class AuditedContexts { if ("setExpectedTag".equals(name) && args != null && args.length == 1 && args[0] instanceof byte[]) { this.verifyMode = true; this.expectedSource = "provided"; + this.tagLen = ((byte[]) args[0]).length; return method.invoke(target, args); } @@ -352,10 +339,7 @@ public final class AuditedContexts { // Update tagLen lazily if not known if (this.tagLen == null) { try { - Object tl = target.getClass().getMethod("tagLength").invoke(target); - if (tl instanceof Integer i) { // NOPMD - this.tagLen = i; - } + this.tagLen = accessors.tagLength(); } catch (Throwable ignore) { // NOPMD this.tagLen = null; } @@ -375,41 +359,23 @@ public final class AuditedContexts { // setPeerPublic(PublicKey) if ("setPeerPublic".equals(name) && args != null && args.length == 1 && args[0] instanceof PublicKey) { + Object result = method.invoke(target, args); try { - Object res = method.invoke(target, args); - try { - String peerFp = fingerprint((Key) args[0]); // short, non-reversible - audit.onAgreementPeerSet(ctxId, peerFp); - } catch (Throwable ignore) { // NOPMD - } - return res; - } catch (Throwable t) { // NOPMD - Throwable cause = unwrapInvocationTarget(t); - try { - audit.onFailure(ctxId, "invoke:setPeerPublic", role.name(), cause); - } catch (Throwable ignore) { // NOPMD - } - throw cause; + String peerFingerprint = fingerprint((Key) args[0]); + audit.onAgreementPeerSet(ctxId, peerFingerprint); + } catch (Throwable ignore) { // NOPMD } + return result; } // deriveSecret() if ("deriveSecret".equals(name) && (args == null || args.length == 0)) { + byte[] secret = (byte[]) method.invoke(target); try { - byte[] secret = (byte[]) method.invoke(target); - try { - audit.onAgreementDerived(ctxId, secret == null ? -1 : secret.length); - } catch (Throwable ignore) { // NOPMD - } - return secret; - } catch (Throwable t) { // NOPMD - Throwable cause = unwrapInvocationTarget(t); - try { - audit.onFailure(ctxId, "invoke:deriveSecret", role.name(), cause); - } catch (Throwable ignore) { // NOPMD - } - throw cause; + audit.onAgreementDerived(ctxId, secret == null ? -1 : secret.length); + } catch (Throwable ignore) { // NOPMD } + return secret; } } @@ -418,41 +384,23 @@ public final class AuditedContexts { // setPeerMessage(byte[]) if ("setPeerMessage".equals(name) && args != null && args.length == 1 && args[0] instanceof byte[]) { + Object result = method.invoke(target, args); try { - Object r = method.invoke(target, args); - try { - int len = ((byte[]) args[0]).length; - audit.onAgreementPeerMessageSet(ctxId, len); - } catch (Throwable ignore) { // NOPMD - } - return r; - } catch (Throwable t) { // NOPMD - Throwable cause = unwrapInvocationTarget(t); - try { - audit.onFailure(ctxId, "invoke:setPeerMessage", role.name(), cause); - } catch (Throwable ignore) { // NOPMD - } - throw cause; + int length = ((byte[]) args[0]).length; + audit.onAgreementPeerMessageSet(ctxId, length); + } catch (Throwable ignore) { // NOPMD } + return result; } // getPeerMessage() if ("getPeerMessage".equals(name) && (args == null || args.length == 0)) { + byte[] message = (byte[]) method.invoke(target); try { - byte[] msg = (byte[]) method.invoke(target); - try { - audit.onAgreementPeerMessageGet(ctxId, msg == null ? -1 : msg.length); - } catch (Throwable ignore) { // NOPMD - } - return msg; - } catch (Throwable t) { // NOPMD - Throwable cause = unwrapInvocationTarget(t); - try { - audit.onFailure(ctxId, "invoke:getPeerMessage", role.name(), cause); - } catch (Throwable ignore) { // NOPMD - } - throw cause; + audit.onAgreementPeerMessageGet(ctxId, message == null ? -1 : message.length); + } catch (Throwable ignore) { // NOPMD } + return message; } } @@ -487,10 +435,6 @@ public final class AuditedContexts { audit.onContextClosed(ctxId, bodyBytes, trailerBytes, durationMs); } catch (Throwable ignore) { // NOPMD } - try { - audit.onContextClosed(algoId, provider, role, keyForClose); - } catch (Throwable ignore) { // NOPMD - } } } @@ -564,7 +508,9 @@ public final class AuditedContexts { final KeyUsage role, final AuditingHandler h) { return new FilterInputStream(in) { private long total; - private boolean eof; + private boolean terminal; + private boolean failureReported; + private boolean verificationReported; @Override public int read() throws IOException { @@ -605,8 +551,10 @@ public final class AuditedContexts { @Override public void close() throws IOException { try { // NOPMD - transferTo(OutputStream.nullOutputStream()); - onEof(); + if (!terminal) { + transferTo(OutputStream.nullOutputStream()); + onEof(); + } } finally { super.close(); } @@ -621,12 +569,17 @@ public final class AuditedContexts { } private void onEof() { - if (eof) { + if (terminal) { return; } - eof = true; + terminal = true; - // Reclassify trailer if tagLen known + if (h.verifyMode) { + reportVerification(true); + return; + } + + // Produce-mode streams emit the tag as a trailer. if (h.tagLen != null && h.tagLen > 0 && h.bodyBytes >= h.tagLen) { h.bodyBytes -= h.tagLen; h.trailerBytes += h.tagLen; @@ -635,36 +588,103 @@ public final class AuditedContexts { } catch (Throwable ignore) { // NOPMD } - // Produce tag or verification result at EOF (no exception thrown) try { - if (h.verifyMode) { - audit.onVerifyResult(ctxId, true, h.policyLabel, h.expectedSource, h.tagLen); - } else { - audit.onTagProduced(ctxId, h.tagLen, h.policyLabel); - } + audit.onTagProduced(ctxId, h.tagLen, h.policyLabel); } catch (Throwable ignore) { // NOPMD } } } private void onFailureMaybeVerify(IOException ioe) { - try { - audit.onFailure(ctxId, "read", role.name(), ioe); - if (h.verifyMode && h.tagLen != null && h.tagLen > 0) { - // Heuristic: verification failures commonly bubble as IOException from the - // engine. - audit.onVerifyResult(ctxId, false, h.policyLabel, h.expectedSource, h.tagLen); + terminal = true; + if (!failureReported) { + failureReported = true; + try { + audit.onFailure(ctxId, "read", role.name(), ioe); + } catch (Throwable ignore) { // NOPMD } + } + if (h.verifyMode) { + reportVerification(false); + } + } + + private void reportVerification(boolean success) { + if (verificationReported) { + return; + } + verificationReported = true; + int reportedTagLength = h.tagLen == null ? -1 : h.tagLen; + try { + audit.onVerifyResult(ctxId, success, h.policyLabel, h.expectedSource, reportedTagLength); } catch (Throwable ignore) { // NOPMD } } }; } - private static Object invokeNoArg(Object target, String method) throws Throwable { - Method m = target.getClass().getMethod(method); - m.setAccessible(true); // NOPMD - return m.invoke(target); + /** + * Handler-local, pre-bound optional no-argument accessors. + */ + private static final class AccessorSet { + private final MethodHandle specAccessor; + private final MethodHandle tagLengthAccessor; + + private AccessorSet(Object target) { + this.specAccessor = bindNoArg(target, "spec", ContextSpec.class); + this.tagLengthAccessor = bindNoArg(target, "tagLength", Integer.class); + } + + private Object spec() throws Throwable { + if (specAccessor == null) { + return null; + } + return specAccessor.invokeExact(); + } + + private Integer tagLength() throws Throwable { + if (tagLengthAccessor == null) { + return null; + } + Object result = tagLengthAccessor.invokeExact(); + return result instanceof Integer ? (Integer) result : null; + } + + private static MethodHandle bindNoArg(Object target, String name, Class expectedType) { + MethodHandle handle = bindNoArg(target, target.getClass().getMethods(), name, expectedType); + if (handle != null) { + return handle; + } + Class[] interfaces = allInterfaces(target.getClass()); + for (Class type : interfaces) { + handle = bindNoArg(target, type.getMethods(), name, expectedType); + if (handle != null) { + return handle; + } + } + return null; + } + + private static MethodHandle bindNoArg(Object target, Method[] methods, String name, Class expectedType) { + for (Method method : methods) { + if (!name.equals(method.getName()) || method.getParameterCount() != 0 + || !compatibleReturnType(method.getReturnType(), expectedType)) { + continue; + } + try { + MethodHandle handle = MethodHandles.publicLookup().unreflect(method).bindTo(target); + return handle.asType(MethodType.methodType(Object.class)); + } catch (IllegalAccessException exception) { // NOPMD - try another public declaration + // Try another public declaration of the same accessor. + } + } + return null; + } + + private static boolean compatibleReturnType(Class actualType, Class expectedType) { + return expectedType == Integer.class && actualType == Integer.TYPE + || expectedType.isAssignableFrom(actualType); + } } private static Throwable unwrapInvocationTarget(Throwable t) { @@ -682,22 +702,41 @@ public final class AuditedContexts { if (key == null) { return "n/a"; } + if (!(key instanceof PublicKey)) { + return safeKeyMetadata(key); + } + + byte[] workingEncoding = null; + byte[] digest = null; try { - byte[] enc = key.getEncoded(); - if (enc == null) { + workingEncoding = key.getEncoded(); + if (workingEncoding == null) { // Non-extractable key: fall back to type information - return key.getAlgorithm() + ":" + key.getClass().getSimpleName(); + return safeKeyMetadata(key); } - MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] d = md.digest(enc); + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + digest = messageDigest.digest(workingEncoding); // hex-short: first 8 bytes - StringBuilder sb = new StringBuilder(2 * 8); - for (int i = 0; i < Math.min(8, d.length); i++) { - sb.append(String.format("%02x", d[i])); + StringBuilder fingerprint = new StringBuilder(16); + for (int index = 0; index < Math.min(8, digest.length); index++) { + int value = digest[index] & 0xff; + fingerprint.append(Character.forDigit(value >>> 4, 16)) + .append(Character.forDigit(value & 0x0f, 16)); } - return key.getAlgorithm() + ":" + sb.toString(); - } catch (NoSuchAlgorithmException e) { + return key.getAlgorithm() + ":" + fingerprint; + } catch (NoSuchAlgorithmException exception) { return key.getAlgorithm() + ":fp-error"; + } finally { + if (workingEncoding != null) { + Arrays.fill(workingEncoding, (byte) 0); + } + if (digest != null) { + Arrays.fill(digest, (byte) 0); + } } } + + private static String safeKeyMetadata(Key key) { + return key.getAlgorithm() + ":" + key.getClass().getSimpleName(); + } } diff --git a/lib/src/main/java/zeroecho/core/audit/JulAuditListenerStd.java b/lib/src/main/java/zeroecho/core/audit/JulAuditListenerStd.java index 34e0b34..b586911 100644 --- a/lib/src/main/java/zeroecho/core/audit/JulAuditListenerStd.java +++ b/lib/src/main/java/zeroecho/core/audit/JulAuditListenerStd.java @@ -37,24 +37,25 @@ import java.security.Key; import java.security.KeyPair; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.util.Arrays; import java.util.Map; import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; import zeroecho.core.KeyUsage; -import zeroecho.core.context.CryptoContext; import zeroecho.core.spec.AlgorithmKeySpec; -import zeroecho.core.spec.ContextSpec; /** * AuditListener implementation that emits structured Java Util Logging records. * *

      * The listener produces parameterized JUL messages in a stable key=value format - * suitable for ingestion by log processors. It never logs secret material: keys - * are represented only by short, non-reversible fingerprints and specification - * objects are summarized by simple type names. + * suitable for ingestion by log processors. It never logs secret material: + * public keys may be represented by short, non-reversible fingerprints, while + * private and secret keys are represented only by algorithm and implementation + * type. Specification objects are summarized by simple type names. *

      * *

      Configuration

      @@ -75,8 +76,8 @@ import zeroecho.core.spec.ContextSpec; * // Progress tick: * PROGRESS ctxId=abc-123 body=4096 trailer=16 * - * // Failure with stack trace when enabled: - * FAILURE ctxId=abc-123 stage=read op=wrap error=IOException message=stream closed + * // Failure summary: + * FAILURE ctxId=abc-123 stage=read op=wrap error=IOException * }
      * *

      @@ -134,7 +135,7 @@ public final class JulAuditListenerStd implements AuditListener { *

    129. {@code infoLevel}: {@code Level.INFO}
    130. *
    131. {@code warnLevel}: {@code Level.WARNING}
    132. *
    133. {@code progressLevel}: {@code Level.FINE}
    134. - *
    135. {@code includeStackTraces}: {@code true}
    136. + *
    137. {@code includeStackTraces}: {@code false}
    138. * * *

      Example

      {@code
      @@ -152,7 +153,7 @@ public final class JulAuditListenerStd implements AuditListener {
               private Level infoLevel = Level.INFO;
               private Level warnLevel = Level.WARNING;
               private Level progressLevel = Level.FINE;
      -        private boolean includeStackTraces = true;
      +        private boolean includeStackTraces;
       
               /**
                * Sets the JUL logger that will receive audit messages.
      @@ -202,6 +203,12 @@ public final class JulAuditListenerStd implements AuditListener {
                * Controls whether {@link #onFailure(String, String, String, Throwable)}
                * appends a stack trace in addition to the structured summary.
                *
      +         * 

      + * Stack traces may contain provider exception messages or application + * values. Enabling them is an explicit diagnostic opt-in and requires a + * suitably protected log destination. + *

      + * * @param include true to include stack traces, false to omit them * @return this builder for chaining */ @@ -229,44 +236,6 @@ public final class JulAuditListenerStd implements AuditListener { return new Builder(); } - /** - * Logs a structured context creation event. - * - * @param context specification type - * @param key type - * @param id algorithm or implementation identifier - * @param provider provider or vendor label - * @param role key usage role for the context - * @param key associated key, if any - * @param spec context specification, if any - */ - @Override - public void onContextCreated(String id, String provider, KeyUsage role, - K key, S spec) { - if (!log.isLoggable(infoLevel)) { - return; - } - log.log(infoLevel, "CTX_CREATED algo={0} provider={1} role={2} keyFp={3} spec={4}", - new Object[] { id, provider, role, fingerprint(key), specName(spec) }); - } - - /** - * Logs a structured context closure event in generic form. - * - * @param id algorithm or implementation identifier - * @param provider provider or vendor label - * @param role key usage role for the context - * @param key associated key, if any - */ - @Override - public void onContextClosed(String id, String provider, KeyUsage role, Key key) { - if (!log.isLoggable(infoLevel)) { - return; - } - log.log(infoLevel, "CTX_CLOSED algo={0} provider={1} role={2} keyFp={3}", - new Object[] { id, provider, role, fingerprint(key) }); - } - /** * Logs key pair generation with non-secret metadata. * @@ -298,7 +267,7 @@ public final class JulAuditListenerStd implements AuditListener { if (!log.isLoggable(infoLevel)) { return; } - log.log(infoLevel, "KEY_BUILT algo={0} provider={1} spec={2} keyFp={3}", + log.log(infoLevel, "KEY_BUILT algo={0} provider={1} spec={2} key={3}", new Object[] { id, provider, specType(spec), fingerprint(key) }); } @@ -314,7 +283,7 @@ public final class JulAuditListenerStd implements AuditListener { if (!log.isLoggable(infoLevel)) { return; } - log.log(infoLevel, "KEY_DESTROYED algo={0} provider={1} keyFp={2}", + log.log(infoLevel, "KEY_DESTROYED algo={0} provider={1} key={2}", new Object[] { id, provider, fingerprint(key) }); } @@ -469,9 +438,8 @@ public final class JulAuditListenerStd implements AuditListener { if (!log.isLoggable(warnLevel)) { return; } - String msg = "FAILURE ctxId={0} stage={1} op={2} error={3} message={4}"; - Object[] params = { ctxId, stage, op, (cause == null ? "unknown" : cause.getClass().getSimpleName()), - (cause == null ? "" : safeMessage(cause.getMessage())) }; + String msg = "FAILURE ctxId={0} stage={1} op={2} error={3}"; + Object[] params = { ctxId, stage, op, (cause == null ? "unknown" : cause.getClass().getSimpleName()) }; if (includeStackTraces && cause != null) { log.log(warnLevel, msg, params); log.log(warnLevel, "STACKTRACE", cause); @@ -522,19 +490,6 @@ public final class JulAuditListenerStd implements AuditListener { } } - /** - * Logs the legacy single-argument creation callback with the context type. - * - * @param ctx the wrapped context - */ - @Override - public void onContextCreated(CryptoContext ctx) { - if (log.isLoggable(progressLevel)) { - log.log(progressLevel, "CTX_CREATED_LEGACY type={0}", - new Object[] { (ctx == null ? "null" : ctx.getClass().getSimpleName()) }); - } - } - /** * Logs the legacy cumulative byte counter. * @@ -598,16 +553,6 @@ public final class JulAuditListenerStd implements AuditListener { } } - /** - * Returns the simple name of the provided specification or "null". - * - * @param spec a context specification, possibly null - * @return simple class name or "null" - */ - private static String specName(ContextSpec spec) { - return spec == null ? "null" : spec.getClass().getSimpleName(); - } - /** * Returns the simple name of the provided key specification or "null". * @@ -629,8 +574,9 @@ public final class JulAuditListenerStd implements AuditListener { } /** - * Computes a short, non-reversible fingerprint for the key without logging raw - * key bytes. Non-extractable keys fall back to algorithm and type. + * Computes a short, non-reversible fingerprint for a public key. Private and + * secret keys are summarized only by algorithm and type, and their encodings + * are never requested. * * @param key the key to summarize; may be null * @return a fingerprint string or "n/a" if the key is null @@ -639,30 +585,34 @@ public final class JulAuditListenerStd implements AuditListener { if (key == null) { return "n/a"; } + if (!(key instanceof PublicKey)) { + return keyType(key); + } + byte[] enc = null; + byte[] digest = null; try { - byte[] enc = key.getEncoded(); + enc = key.getEncoded(); if (enc == null) { return key.getAlgorithm() + ":" + key.getClass().getSimpleName(); } MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] d = md.digest(enc); + digest = md.digest(enc); StringBuilder sb = new StringBuilder(key.getAlgorithm()).append(':'); - for (int i = 0; i < Math.min(8, d.length); i++) { - sb.append(String.format("%02x", d[i])); + for (int i = 0; i < Math.min(8, digest.length); i++) { + int value = digest[i] & 0xff; + sb.append(Character.forDigit(value >>> 4, 16)) + .append(Character.forDigit(value & 0x0f, 16)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { return key.getAlgorithm() + ":fp-error"; + } finally { + if (enc != null) { + Arrays.fill(enc, (byte) 0); + } + if (digest != null) { + Arrays.fill(digest, (byte) 0); + } } } - - /** - * Returns a non-null message string for logging. - * - * @param s a message, possibly null - * @return the message or an empty string if null - */ - private static String safeMessage(String s) { - return s == null ? "" : s; - } } diff --git a/lib/src/main/java/zeroecho/core/context/AgreementContext.java b/lib/src/main/java/zeroecho/core/context/AgreementContext.java index 7d2f77b..834c0b0 100644 --- a/lib/src/main/java/zeroecho/core/context/AgreementContext.java +++ b/lib/src/main/java/zeroecho/core/context/AgreementContext.java @@ -49,8 +49,8 @@ import java.security.PublicKey; * *

      Lifecycle

      *
        - *
      • Create a context via {@code CryptoAlgorithm#create(...)} or - * {@code CryptoAlgorithms.create(...)} for the {@code AGREEMENT} role.
      • + *
      • Create a context via {@code ZeroEchoSession#createContext(...)} for the + * {@code AGREEMENT} role.
      • *
      • Call {@link #setPeerPublic(PublicKey)} with the peer’s public key.
      • *
      • Invoke {@link #deriveSecret()} once to compute the raw shared * secret.
      • diff --git a/lib/src/main/java/zeroecho/core/context/CryptoContext.java b/lib/src/main/java/zeroecho/core/context/CryptoContext.java index 8392e3b..64090c3 100644 --- a/lib/src/main/java/zeroecho/core/context/CryptoContext.java +++ b/lib/src/main/java/zeroecho/core/context/CryptoContext.java @@ -52,8 +52,7 @@ import zeroecho.core.CryptoAlgorithm; * *

        Lifecycle

        *
          - *
        • Contexts are created via {@code CryptoAlgorithm#create(...)} or the - * convenience methods in {@code CryptoAlgorithms}.
        • + *
        • Contexts are created via {@code ZeroEchoSession#createContext(...)}.
        • *
        • They may wrap native or provider-managed resources that must be * released.
        • *
        • Once closed, a context must not be reused; callers should request a new @@ -62,9 +61,8 @@ import zeroecho.core.CryptoAlgorithm; * *

          Security considerations

          *
            - *
          • Closing a context may attempt to destroy the underlying {@link Key} if it - * implements {@code javax.security.auth.Destroyable} and auditing is - * enabled.
          • + *
          • Bound keys are borrowed from the caller and are never destroyed by + * context closure.
          • *
          • Applications should always call {@link #close()} promptly to avoid * leaking key material or other sensitive state.
          • *
          • Contexts are not guaranteed to be thread-safe; concurrent use should be @@ -110,9 +108,10 @@ public sealed interface CryptoContext extends Closeable * Closes this context and releases all associated resources. * *

            - * Implementations should free provider state and native handles. If the bound - * key supports destruction, the library may attempt to invoke {@code destroy()} - * on it when auditing is enabled. + * Implementations free context-owned provider state and native handles. The + * bound key remains caller-owned: closing a context never destroys the key. + * Callers that own a destroyable key must destroy it explicitly after all + * contexts and other consumers have released it. *

            * * @throws java.io.IOException if the underlying provider encounters an I/O diff --git a/lib/src/main/java/zeroecho/core/context/EncryptionContext.java b/lib/src/main/java/zeroecho/core/context/EncryptionContext.java index c5f5bf1..088f047 100644 --- a/lib/src/main/java/zeroecho/core/context/EncryptionContext.java +++ b/lib/src/main/java/zeroecho/core/context/EncryptionContext.java @@ -48,7 +48,7 @@ import java.io.InputStream; * *

            Lifecycle

            *
              - *
            • Create a context via {@code CryptoAlgorithm#create(...)} for the + *
            • Create a context via {@code ZeroEchoSession#createContext(...)} for the * {@link zeroecho.core.KeyUsage#ENCRYPT} or * {@link zeroecho.core.KeyUsage#DECRYPT} role.
            • *
            • Call {@link #attach(InputStream)} to obtain a wrapped stream.
            • diff --git a/lib/src/main/java/zeroecho/core/context/KemContext.java b/lib/src/main/java/zeroecho/core/context/KemContext.java index 11c343d..215df52 100644 --- a/lib/src/main/java/zeroecho/core/context/KemContext.java +++ b/lib/src/main/java/zeroecho/core/context/KemContext.java @@ -46,7 +46,7 @@ import java.io.IOException; * *

              Lifecycle

              *
                - *
              • Create a context via {@code CryptoAlgorithm#create(...)} for the + *
              • Create a context via {@code ZeroEchoSession#createContext(...)} for the * {@link zeroecho.core.KeyUsage#ENCAPSULATE} or * {@link zeroecho.core.KeyUsage#DECAPSULATE} role.
              • *
              • Call {@link #encapsulate()} when acting as an initiator. The result diff --git a/lib/src/main/java/zeroecho/core/context/MessageAgreementContext.java b/lib/src/main/java/zeroecho/core/context/MessageAgreementContext.java index 5b65dcf..8044c06 100644 --- a/lib/src/main/java/zeroecho/core/context/MessageAgreementContext.java +++ b/lib/src/main/java/zeroecho/core/context/MessageAgreementContext.java @@ -57,7 +57,7 @@ package zeroecho.core.context; * *

                Lifecycle

                *
                  - *
                • Create a context via {@code CryptoAlgorithm#create(...)} for the + *
                • Create a context via {@code ZeroEchoSession#createContext(...)} for the * {@link zeroecho.core.KeyUsage#AGREEMENT} role.
                • *
                • Initiators call {@link #getPeerMessage()} to obtain the message to send * to the responder, then invoke {@link AgreementContext#deriveSecret()}.
                • diff --git a/lib/src/main/java/zeroecho/core/err/UnsupportedRoleException.java b/lib/src/main/java/zeroecho/core/err/UnsupportedRoleException.java index c5dfd25..3711286 100644 --- a/lib/src/main/java/zeroecho/core/err/UnsupportedRoleException.java +++ b/lib/src/main/java/zeroecho/core/err/UnsupportedRoleException.java @@ -46,11 +46,13 @@ package zeroecho.core.err; *

                  When it is thrown

                  *
                    *
                  • During - * {@link zeroecho.core.CryptoAlgorithms#create(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)} + * {@link zeroecho.sdk.ZeroEchoSession#createContext(String, + * zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)} * after policy validation, if the resolved algorithm exposes no bindings for * the given role.
                  • *
                  • Directly from - * {@link zeroecho.core.CryptoAlgorithm#create(zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)} + * {@link zeroecho.core.CryptoAlgorithm#createContext(zeroecho.core.KeyUsage, + * java.security.Key, zeroecho.core.spec.ContextSpec)} * when no binding exists for the role.
                  • *
                  * @@ -61,10 +63,10 @@ package zeroecho.core.err; * *

                  Example

                  {@code
                    * // Suppose "SHA-256" supports DIGEST only.
                  - * var algo = zeroecho.core.CryptoAlgorithms.require("SHA-256");
                  + * zeroecho.sdk.ZeroEchoSession session = new zeroecho.sdk.ZeroEchoSession();
                    * try {
                    *     // Asking for ENCRYPT on a digest algorithm will fail with UnsupportedRoleException.
                  - *     zeroecho.core.CryptoAlgorithms.create("SHA-256", zeroecho.core.KeyUsage.ENCRYPT,
                  + *     session.createContext("SHA-256", zeroecho.core.KeyUsage.ENCRYPT,
                    *         zeroecho.core.NullKey.INSTANCE, null);
                    * } catch (UnsupportedRoleException e) {
                    *     // Handle: algorithm does not implement the ENCRYPT role.
                  diff --git a/lib/src/main/java/zeroecho/core/err/UnsupportedSpecException.java b/lib/src/main/java/zeroecho/core/err/UnsupportedSpecException.java
                  index 7267438..7b2f269 100644
                  --- a/lib/src/main/java/zeroecho/core/err/UnsupportedSpecException.java
                  +++ b/lib/src/main/java/zeroecho/core/err/UnsupportedSpecException.java
                  @@ -38,7 +38,9 @@ package zeroecho.core.err;
                    * the algorithm for the requested role.
                    *
                    * 

                  - * This is thrown by {@link zeroecho.core.CryptoAlgorithm#create} when: + * This is thrown by + * {@link zeroecho.core.CryptoAlgorithm#createContext(zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)} + * when: *

                  *
                    *
                  • The algorithm supports the requested {@link zeroecho.core.KeyUsage} role, @@ -58,7 +60,7 @@ package zeroecho.core.err; * CryptoAlgorithm aes = CryptoAlgorithms.require("AES/GCM"); * SecretKey wrongKey = ... // an RSA key by mistake * try { - * aes.create(KeyUsage.ENCRYPT, wrongKey, null); + * aes.createContext(KeyUsage.ENCRYPT, wrongKey, null); * } catch (UnsupportedSpecException e) { * // no binding accepted the RSA key for ENCRYPT role * } diff --git a/lib/src/main/java/zeroecho/core/err/package-info.java b/lib/src/main/java/zeroecho/core/err/package-info.java index fabb46a..1f4c955 100644 --- a/lib/src/main/java/zeroecho/core/err/package-info.java +++ b/lib/src/main/java/zeroecho/core/err/package-info.java @@ -67,7 +67,7 @@ *

                    Typical usage

                    {@code
                      * try {
                      *     zeroecho.core.context.EncryptionContext ctx =
                    - *         algo.create(zeroecho.core.KeyUsage.ENCRYPT, key, spec);
                    + *         algo.createContext(zeroecho.core.KeyUsage.ENCRYPT, key, spec);
                      *     // use ctx...
                      * } catch (zeroecho.core.err.UnsupportedRoleException
                      *        | zeroecho.core.err.UnsupportedSpecException e) {
                    diff --git a/lib/src/main/java/zeroecho/core/io/AbstractChunkTransformInputStream.java b/lib/src/main/java/zeroecho/core/io/AbstractChunkTransformInputStream.java
                    index f2abfdc..bf3088b 100644
                    --- a/lib/src/main/java/zeroecho/core/io/AbstractChunkTransformInputStream.java
                    +++ b/lib/src/main/java/zeroecho/core/io/AbstractChunkTransformInputStream.java
                    @@ -123,6 +123,7 @@ import java.io.InputStream;
                      * @since 1.0
                      */
                     public abstract class AbstractChunkTransformInputStream extends FilterInputStream {
                    +    private static final int MIN_INPUT_CHUNK_SIZE = 2;
                         /** Input buffer storing data read from the upstream stream. */
                         protected final byte[] inBuf;
                         /** Output buffer storing transformed bytes awaiting consumption. */
                    @@ -156,15 +157,14 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
                          * @param outChunkSize size of output chunks produced by the transform (must be
                          *                     > 0)
                          * @param chunks       number of chunks buffered at once (must be > 0)
                    -     * @throws AssertionError if {@code chunks <= 0}, {@code inChunkSize <= 1}, or
                    -     *                        {@code outChunkSize <= 0}
                    +     * @throws IllegalArgumentException if a size is outside its documented range
                    +     *                                  or a buffer size overflows
                          */
                         protected AbstractChunkTransformInputStream(InputStream upstream, int inChunkSize, int outChunkSize, int chunks) {
                             super(upstream);
                    -        assert chunks > 0 && inChunkSize > 1 && outChunkSize > 0;
                    -
                    -        this.inBuf = new byte[inChunkSize * chunks];
                    -        this.outBuf = new byte[outChunkSize * chunks];
                    +        validateGeometry(inChunkSize, outChunkSize, chunks, 0);
                    +        this.inBuf = new byte[checkedMultiply(inChunkSize, chunks, "input buffer size")];
                    +        this.outBuf = new byte[checkedMultiply(outChunkSize, chunks, "output buffer size")];
                     
                             this.inChunkSize = inChunkSize;
                             this.outChunkSize = outChunkSize;
                    @@ -190,16 +190,16 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
                          *                                 steady-state (must be > 0)
                          * @param finalizationOutputChunks number of extra output chunks reserved for
                          *                                 finalization (must be >= 0)
                    -     * @throws AssertionError if {@code chunks <= 0}, {@code inChunkSize <= 1}, or
                    -     *                        {@code outChunkSize <= 0}
                    +     * @throws IllegalArgumentException if a size is outside its documented range
                    +     *                                  or a buffer size overflows
                          */
                         protected AbstractChunkTransformInputStream(InputStream upstream, int inChunkSize, int outChunkSize, int chunks,
                                 int finalizationOutputChunks) {
                             super(upstream);
                    -        assert chunks > 0 && inChunkSize > 1 && outChunkSize > 0;
                    -
                    -        this.inBuf = new byte[inChunkSize * chunks];
                    -        this.outBuf = new byte[outChunkSize * (chunks + finalizationOutputChunks)];
                    +        validateGeometry(inChunkSize, outChunkSize, chunks, finalizationOutputChunks);
                    +        int outputChunks = checkedAdd(chunks, finalizationOutputChunks, "output chunk count");
                    +        this.inBuf = new byte[checkedMultiply(inChunkSize, chunks, "input buffer size")];
                    +        this.outBuf = new byte[checkedMultiply(outChunkSize, outputChunks, "output buffer size")];
                     
                             this.inChunkSize = inChunkSize;
                             this.outChunkSize = outChunkSize;
                    @@ -252,14 +252,17 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
                          * @throws IOException if the upstream read or the transformation fails
                          */
                         private boolean fillBuffers() throws IOException {
                    -        assert outPtr == outLen;
                    +        if (outPtr != outLen) {
                    +            throw new IllegalStateException("Output buffer was refilled before it was drained");
                    +        }
                     
                             inLen = in.readNBytes(inBuf, 0, inBuf.length);
                             if (inLen == 0) {
                                 // EOF: run finalization exactly once, even if there's no remainder,
                                 // and surface any produced bytes (e.g., padding block, GCM tag).
                                 if (!eofSeen) {
                    -                int finalOut = doFinal(inBuf, 0, 0, outBuf, 0);
                    +                int finalOut = validateOutputCount(doFinal(inBuf, 0, 0, outBuf, 0), outBuf.length,
                    +                        "finalization");
                                     outPtr = 0;
                                     outLen = finalOut;
                                     eofSeen = true;
                    @@ -270,14 +273,16 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
                     
                             // all chunks are aligned to the specified boundary (inChunkSize) -> transform
                             // can be simply invoked
                    -        outLen = transform(inBuf, 0, inLen / inChunkSize, outBuf);
                    +        outLen = validateOutputCount(transform(inBuf, 0, inLen / inChunkSize, outBuf), outBuf.length,
                    +                "transformation");
                             outPtr = 0;
                     
                             int left = inLen % inChunkSize;
                     
                             if (left > 0) {
                                 int finalOutChunkSize = doFinal(inBuf, inLen - left, left, outBuf, outLen);
                    -            outLen = outLen + finalOutChunkSize;
                    +            finalOutChunkSize = validateOutputCount(finalOutChunkSize, outBuf.length - outLen, "finalization");
                    +            outLen = checkedAddState(outLen, finalOutChunkSize);
                                 // we ask for whole inBufSize chunks, if readNBytes returns a partial chunk, it
                                 // must be eof
                                 eofSeen = true;
                    @@ -286,6 +291,53 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
                             return true;
                         }
                     
                    +    private static void validateGeometry(int inChunkSize, int outChunkSize, int chunks,
                    +            int finalizationOutputChunks) {
                    +        if (inChunkSize < MIN_INPUT_CHUNK_SIZE) {
                    +            throw new IllegalArgumentException("inChunkSize must be greater than 1");
                    +        }
                    +        if (outChunkSize <= 0) {
                    +            throw new IllegalArgumentException("outChunkSize must be greater than 0");
                    +        }
                    +        if (chunks <= 0) {
                    +            throw new IllegalArgumentException("chunks must be greater than 0");
                    +        }
                    +        if (finalizationOutputChunks < 0) {
                    +            throw new IllegalArgumentException("finalizationOutputChunks must not be negative");
                    +        }
                    +    }
                    +
                    +    private static int checkedMultiply(int left, int right, String description) {
                    +        try {
                    +            return Math.multiplyExact(left, right);
                    +        } catch (ArithmeticException exception) {
                    +            throw new IllegalArgumentException(description + " exceeds the supported range", exception);
                    +        }
                    +    }
                    +
                    +    private static int checkedAdd(int left, int right, String description) {
                    +        try {
                    +            return Math.addExact(left, right);
                    +        } catch (ArithmeticException exception) {
                    +            throw new IllegalArgumentException(description + " exceeds the supported range", exception);
                    +        }
                    +    }
                    +
                    +    private static int checkedAddState(int left, int right) {
                    +        try {
                    +            return Math.addExact(left, right);
                    +        } catch (ArithmeticException exception) {
                    +            throw new IllegalStateException("Transform output length overflowed", exception);
                    +        }
                    +    }
                    +
                    +    private static int validateOutputCount(int count, int capacity, String operation) {
                    +        if (count < 0 || count > capacity) {
                    +            throw new IllegalStateException(operation + " returned an invalid output count: " + count);
                    +        }
                    +        return count;
                    +    }
                    +
                         /**
                          * Reads the next transformed byte.
                          *
                    @@ -295,11 +347,12 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
                          */
                         @Override
                         public int read() throws IOException {
                    -        if (outPtr < outLen) {
                    -            return outBuf[outPtr++] & 0xff;
                    +        while (outPtr >= outLen) {
                    +            if (!fillBuffers()) {
                    +                return -1;
                    +            }
                             }
                    -
                    -        return fillBuffers() ? outBuf[outPtr++] & 0xff : -1 /* eof */;
                    +        return outBuf[outPtr++] & 0xff;
                         }
                     
                         /**
                    diff --git a/lib/src/main/java/zeroecho/core/io/CipherTransformInputStreamBuilder.java b/lib/src/main/java/zeroecho/core/io/CipherTransformInputStreamBuilder.java
                    index 04845e9..c1ca971 100644
                    --- a/lib/src/main/java/zeroecho/core/io/CipherTransformInputStreamBuilder.java
                    +++ b/lib/src/main/java/zeroecho/core/io/CipherTransformInputStreamBuilder.java
                    @@ -48,15 +48,16 @@ import javax.crypto.Cipher;
                      * {@link Cipher}. Three stream variants are available:
                      * 

                    *
                      - *
                    • SmartBlockStream - invokes + *
                    • Independent block stream - invokes * {@link Cipher#doFinal(byte[], int, int, byte[], int)} for each full input * block; a final partial block (if any) is processed by a single - * {@code doFinal}.
                    • - *
                    • SmartPaddedBlockStream - like {@code SmartBlockStream}, but + * {@code doFinal}. This mode is restricted to RSA and ElGamal.
                    • + *
                    • Left-padded independent block stream - like the independent block + * stream, but * left-pads each transformed output block with zeros up to * {@code outChunkSize}. Final blocks must be complete; otherwise an * {@link IllegalStateException} is thrown.
                    • - *
                    • SmartContinuousBlockStream - streaming variant that uses + *
                    • Continuous stream - uses * {@code Cipher.update(...)} for bulk bytes and a single {@code doFinal()} at * end of stream. This is suitable for CTR/CFB/OFB/GCM and padding modes.
                    • *
                    @@ -128,7 +129,7 @@ import javax.crypto.Cipher; * InputStream s4 = CipherTransformInputStreamBuilder.builder() * .withUpstream(in) * .withCipher(c4) - * .withUpdateStreaming(true) // provider typically accepts update+doFinal + * .withIndependentBlocks() * .withInputBlockSize(elgIn) * .withOutputBlockSize(elgOut) * .withBufferedBlocks(200) @@ -313,6 +314,24 @@ public final class CipherTransformInputStreamBuilder { return this; } + /** + * Selects independent-block processing, in which every logical input block is + * passed to a separate {@link Cipher#doFinal(byte[], int, int, byte[], int)} + * invocation. + * + *

                    + * This mode is supported only for RSA and ElGamal transformations. Stateful + * symmetric modes, including AES-GCM and AES-CBC, must use + * {@link #withUpdateStreaming()}. + *

                    + * + * @return this builder + */ + public CipherTransformInputStreamBuilder withIndependentBlocks() { + this.updateStreaming = false; + return this; + } + /** * Builds a chunk-transforming {@link InputStream} using the configured options. * @@ -326,7 +345,9 @@ public final class CipherTransformInputStreamBuilder { * * @return a new InputStream that transforms bytes on the fly * @throws NullPointerException if {@code upstream} or {@code cipher} is null - * @throws AssertionError if buffer sizing assertions fail + * @throws IllegalArgumentException if independent-block processing is selected + * for an unsupported algorithm or buffer + * geometry is invalid */ public InputStream build() { Objects.requireNonNull(upstream, "upstream must not be null"); @@ -336,7 +357,18 @@ public final class CipherTransformInputStreamBuilder { return new SmartContinuousBlockStream(upstream, cipher, inChunkSize, outChunkSize, bufferedBlocks, finalizationOutputChunks); } + validateIndependentBlockAlgorithm(cipher); return padding ? new SmartPaddedBlockStream(upstream, cipher, inChunkSize, outChunkSize, bufferedBlocks) : new SmartBlockStream(upstream, cipher, inChunkSize, outChunkSize, bufferedBlocks); } + + private static void validateIndependentBlockAlgorithm(Cipher cipher) { + String transformation = cipher.getAlgorithm(); + int separator = transformation.indexOf('/'); + String baseAlgorithm = separator < 0 ? transformation : transformation.substring(0, separator); + if (!"RSA".equalsIgnoreCase(baseAlgorithm) && !"ElGamal".equalsIgnoreCase(baseAlgorithm)) { + throw new IllegalArgumentException( + "Independent-block processing supports only RSA and ElGamal transformations"); + } + } } diff --git a/lib/src/main/java/zeroecho/core/io/SmartBlockStream.java b/lib/src/main/java/zeroecho/core/io/SmartBlockStream.java index f0fa9b9..876c195 100644 --- a/lib/src/main/java/zeroecho/core/io/SmartBlockStream.java +++ b/lib/src/main/java/zeroecho/core/io/SmartBlockStream.java @@ -58,8 +58,6 @@ final class SmartBlockStream extends AbstractChunkTransformInputStream { private static final Logger LOG = Logger.getLogger(SmartBlockStream.class.getName()); private final Cipher cipher; - private boolean doFinalCalled; - /* package */ SmartBlockStream(InputStream upstream, Cipher cipher, int inChunkSize, int outChunkSize, int bufferedBlocks) { super(upstream, inChunkSize, outChunkSize, bufferedBlocks); @@ -81,7 +79,6 @@ final class SmartBlockStream extends AbstractChunkTransformInputStream { protected int transform(byte[] in, int inOff, int inChunks, byte[] out) throws IOException { try { int output = 0; - doFinalCalled = inChunks > 0; for (int i = 0; i < inChunks; i++) { int outOne = cipher.doFinal(in, inOff, inChunkSize, out, output); output = output + outOne; @@ -108,11 +105,10 @@ final class SmartBlockStream extends AbstractChunkTransformInputStream { @Override protected int doFinal(byte[] in, int inOff, int len, byte[] out, int outOff) throws IOException { try { - if (doFinalCalled && len == 0) { + if (len == 0) { return 0; } - doFinalCalled = true; return cipher.doFinal(in, inOff, len, out, outOff); } catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException e) { LOG.logp(Level.WARNING, "SmartBlockStream", "transform", "Exception", e); diff --git a/lib/src/main/java/zeroecho/core/io/SmartContinuousBlockStream.java b/lib/src/main/java/zeroecho/core/io/SmartContinuousBlockStream.java index cb35c78..7dd8b19 100644 --- a/lib/src/main/java/zeroecho/core/io/SmartContinuousBlockStream.java +++ b/lib/src/main/java/zeroecho/core/io/SmartContinuousBlockStream.java @@ -157,12 +157,18 @@ final class SmartContinuousBlockStream extends AbstractChunkTransformInputStream protected int doFinal(byte[] in, int inOff, int len, byte[] out, int outOff) throws IOException { try { int finBlockSize = cipher.getOutputSize(len); - if (out.length < outOff + finBlockSize) { + final int required; + try { + required = Math.addExact(outOff, finBlockSize); + } catch (ArithmeticException exception) { + throw new IOException("Final cipher output size exceeds the supported range", exception); + } + if (out.length < required) { if (LOG.isLoggable(Level.WARNING)) { LOG.log(Level.WARNING, "Expanding buffer of {0} from {1} bytes to {2} bytes", - new Object[] { cipher.getAlgorithm(), outBuf.length, outOff + finBlockSize }); + new Object[] { cipher.getAlgorithm(), outBuf.length, required }); } - out = outBuf = Arrays.copyOf(outBuf, outOff + finBlockSize); // NOPMD + out = outBuf = Arrays.copyOf(outBuf, required); // NOPMD } int written = cipher.doFinal(in, inOff, len, out, outOff); diff --git a/lib/src/main/java/zeroecho/core/io/SmartPaddedBlockStream.java b/lib/src/main/java/zeroecho/core/io/SmartPaddedBlockStream.java index 34424b0..0087cbe 100644 --- a/lib/src/main/java/zeroecho/core/io/SmartPaddedBlockStream.java +++ b/lib/src/main/java/zeroecho/core/io/SmartPaddedBlockStream.java @@ -59,8 +59,6 @@ final class SmartPaddedBlockStream extends AbstractChunkTransformInputStream { private static final Logger LOG = Logger.getLogger(SmartPaddedBlockStream.class.getName()); private final Cipher cipher; - private boolean doFinalCalled; - /* package */ SmartPaddedBlockStream(InputStream upstream, Cipher cipher, int inChunkSize, int outChunkSize, int bufferedBlocks) { super(upstream, inChunkSize, outChunkSize, bufferedBlocks); @@ -83,9 +81,11 @@ final class SmartPaddedBlockStream extends AbstractChunkTransformInputStream { try { // return cipher.doFinal(in, inOff, inChunks * g.inputBlockSize(), out); int output = 0; - doFinalCalled = inChunks > 0; for (int i = 0; i < inChunks; i++) { int outOne = cipher.doFinal(in, inOff, inChunkSize, out, output); + if (outOne < 0 || outOne > outChunkSize) { + throw new IOException("Cipher output exceeds the configured block size"); + } int diff = outChunkSize - outOne; if (diff > 0) { System.arraycopy(out, output, out, output + diff, outOne); @@ -117,7 +117,7 @@ final class SmartPaddedBlockStream extends AbstractChunkTransformInputStream { */ @Override protected int doFinal(byte[] in, int inOff, int len, byte[] out, int outOff) throws IOException { - if (doFinalCalled && len == 0) { + if (len == 0) { return 0; } @@ -125,7 +125,6 @@ final class SmartPaddedBlockStream extends AbstractChunkTransformInputStream { throw new IllegalStateException("Cannot process incomplete blocks: " + len + " instead of " + inChunkSize); } try { - doFinalCalled = true; return cipher.doFinal(in, inOff, len, out, outOff); } catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException e) { LOG.logp(Level.WARNING, "SmartBlockStream", "transform", "Exception", e); diff --git a/lib/src/main/java/zeroecho/core/io/Util.java b/lib/src/main/java/zeroecho/core/io/Util.java index fc1599a..67b8819 100644 --- a/lib/src/main/java/zeroecho/core/io/Util.java +++ b/lib/src/main/java/zeroecho/core/io/Util.java @@ -79,6 +79,8 @@ public final class Util { // NOPMD *

                    */ private static final int DEFAULT_BUFFER_SIZE = 32 * 1024; + /** Largest unsigned 32-bit value accepted by the packed integer decoder. */ + private static final long MAX_PACKED_INTEGER = 0xffff_ffffL; /** * Private constructor to prevent instantiation of this utility class. @@ -271,15 +273,29 @@ public final class Util { // NOPMD * @throws IOException if an I/O error occurs or if the stream ends prematurely */ public static int readPack7I(final InputStream in) throws IOException { - int result = in.read(); - if (result > 0x7f) { // NOPMD - return result & 0x7f; + int current = in.read(); + if (current < 0) { + throw new EOFException("read packed integer EOF"); } - int i; - for (i = in.read(); i < 0x80; i = in.read()) { - result = (result << 7) | i; + if (current > 0x7f) { // NOPMD + return current & 0x7f; } - return (result << 7) | (i & 0x7f); + + long result = current; + for (int bytes = 1; bytes < 5; bytes++) { + current = in.read(); + if (current < 0) { + throw new EOFException("read packed integer EOF"); + } + result = (result << 7) | (current & 0x7f); + if (current > 0x7f) { // NOPMD + if (result > MAX_PACKED_INTEGER) { + throw new IOException("packed integer exceeds 32 bits"); + } + return (int) result; + } + } + throw new IOException("packed integer exceeds five bytes"); } /** diff --git a/lib/src/main/java/zeroecho/core/io/package-info.java b/lib/src/main/java/zeroecho/core/io/package-info.java index 3f8fcde..09ed79a 100644 --- a/lib/src/main/java/zeroecho/core/io/package-info.java +++ b/lib/src/main/java/zeroecho/core/io/package-info.java @@ -53,10 +53,12 @@ * invokes {@code update(...)} on each chunk, optionally emits a single trailer, * then calls {@code onCompleted()} exactly once at EOF.
                  • *
                  • {@link CipherTransformInputStreamBuilder} - fluent builder that creates - * cipher-backed streams for block-per-doFinal, left-zero-padded blocks, or + * cipher-backed streams for RSA/ElGamal independent-block processing, + * left-zero-padded independent blocks, or * continuous {@code update}+{@code doFinal} streaming.
                  • *
                  • {@link SmartBlockStream}, {@link SmartPaddedBlockStream}, - * {@link SmartContinuousBlockStream} - concrete cipher-backed stream variants + * {@link SmartContinuousBlockStream} - internal cipher-backed stream variants; + * the first two are restricted to independent RSA or ElGamal blocks * used by the builder.
                  • *
                  • {@link TailStrippingInputStream} - withholds the last N bytes from the * payload and delivers them to a callback at EOF (useful for tags, checksums, diff --git a/lib/src/main/java/zeroecho/core/marshal/PairSeq.java b/lib/src/main/java/zeroecho/core/marshal/PairSeq.java index 35bd702..b0a72fd 100644 --- a/lib/src/main/java/zeroecho/core/marshal/PairSeq.java +++ b/lib/src/main/java/zeroecho/core/marshal/PairSeq.java @@ -36,7 +36,6 @@ package zeroecho.core.marshal; import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; -import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.List; @@ -62,8 +61,8 @@ import java.util.List; * *

                    Serialization

                    *
                      - *
                    • {@link #writeTo(Appendable)} outputs each pair as {@code k=v\n} lines - * without escaping.
                    • + *
                    • {@link #writeTo(Appendable)} outputs each pair as {@code k=v\n} + * lines without escaping and reports checked I/O failures.
                    • *
                    • {@link #readFrom(java.io.Reader)} parses lines in the same format, * ignoring blank lines and comments starting with {@code #}.
                    • *
                    @@ -86,8 +85,8 @@ public final class PairSeq { * * @param kv alternating key and value strings; must have even length * @return new {@code PairSeq} with the given contents - * @throws IllegalArgumentException if {@code kv} is {@code null} or has odd - * length + * @throws IllegalArgumentException if {@code kv} is {@code null}, has odd + * length, or contains a null key or value */ public static PairSeq of(String... kv) { if (kv == null) { @@ -96,7 +95,15 @@ public final class PairSeq { if ((kv.length & 1) != 0) { throw new IllegalArgumentException("kv must have even length (k,v pairs)"); } - return new PairSeq(kv); + for (int elementIndex = 0; elementIndex < kv.length; elementIndex++) { + if (kv[elementIndex] == null) { + int pairIndex = elementIndex >>> 1; + String role = (elementIndex & 1) == 0 ? "key" : "value"; + throw new IllegalArgumentException( + "pair " + pairIndex + " " + role + " must not be null"); + } + } + return new PairSeq(kv.clone()); } /** @@ -194,7 +201,8 @@ public final class PairSeq { } /** - * Appends all pairs to the target as {@code key=value} lines. + * Appends all pairs to the target as {@code key=value} lines, reporting + * checked I/O failures directly. * *

                    * No escaping is performed; callers must ensure keys and values do not contain @@ -202,15 +210,11 @@ public final class PairSeq { *

                    * * @param out appendable target - * @throws UncheckedIOException if the append fails + * @throws IOException if the append fails */ - public void writeTo(Appendable out) { - try { - for (int i = 0; i < size(); i++) { - out.append(keyAt(i)).append('=').append(valAt(i)).append('\n'); - } - } catch (IOException e) { - throw new UncheckedIOException(e); + public void writeTo(Appendable out) throws IOException { + for (int i = 0; i < size(); i++) { + out.append(keyAt(i)).append('=').append(valAt(i)).append('\n'); } } @@ -244,6 +248,6 @@ public final class PairSeq { list.add(k); list.add(v); } - return new PairSeq(list.toArray(String[]::new)); + return of(list.toArray(String[]::new)); } } diff --git a/lib/src/main/java/zeroecho/core/marshal/PairSeqCodec.java b/lib/src/main/java/zeroecho/core/marshal/PairSeqCodec.java index 54f9d6f..c6e280f 100644 --- a/lib/src/main/java/zeroecho/core/marshal/PairSeqCodec.java +++ b/lib/src/main/java/zeroecho/core/marshal/PairSeqCodec.java @@ -33,10 +33,15 @@ ******************************************************************************/ package zeroecho.core.marshal; -import java.lang.reflect.Constructor; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; /** * Reflection-based {@link Codec} that marshals and unmarshals domain objects to @@ -95,14 +100,29 @@ import java.util.Objects; * User u = codec.unmarshal(repr); * }
                  * - *

                  Thread-safety

                  Instances are immutable and thread-safe. Reflection - * lookups are performed per call and are not cached. + *

                  Thread-safety

                  Instances are immutable and thread-safe. Public + * accessors are resolved once per runtime class and operation type, then invoked + * through cached method handles. The unload-safe {@link ClassValue} caches do + * not retain otherwise unreachable class loaders. * * @param domain type that follows the marshalling and unmarshalling * conventions * @since 1.0 */ public final class PairSeqCodec implements Codec { + private static final ClassValue> MARSHAL_PLANS = new ClassValue<>() { + @Override + protected PlanHolder computeValue(Class type) { + return new PlanHolder<>(() -> MarshalPlan.resolve(type)); + } + }; + private static final ClassValue> UNMARSHAL_PLANS = new ClassValue<>() { + @Override + protected PlanHolder computeValue(Class type) { + return new PlanHolder<>(() -> UnmarshalPlan.resolve(type)); + } + }; + private final Class type; /** @@ -136,17 +156,7 @@ public final class PairSeqCodec implements Codec { @Override public PairSeq marshal(T value) { Objects.requireNonNull(value, "value"); - try { - Method m = value.getClass().getMethod("marshal"); - if (!PairSeq.class.isAssignableFrom(m.getReturnType())) { - throw new IllegalStateException("marshal() must return PairSeq in " + value.getClass().getName()); - } - return (PairSeq) m.invoke(value); - } catch (NoSuchMethodException e) { - throw new IllegalStateException(value.getClass().getName() + " must implement marshal():PairSeq", e); - } catch (IllegalAccessException | InvocationTargetException t) { - throw new IllegalStateException("marshal() failed for " + value.getClass().getName(), t); - } + return MARSHAL_PLANS.get(value.getClass()).get().invoke(value); } /** @@ -174,31 +184,181 @@ public final class PairSeqCodec implements Codec { * constructor exists, or if either invocation * fails */ - @SuppressWarnings("unchecked") @Override public T unmarshal(PairSeq repr) { Objects.requireNonNull(repr, "repr"); - // Prefer static unmarshal(PairSeq) - try { - Method m = type.getMethod("unmarshal", PairSeq.class); - if ((m.getModifiers() & java.lang.reflect.Modifier.STATIC) != 0) { - return (T) m.invoke(null, repr); - } - } catch (NoSuchMethodException ignore) { // NOPMD - // fall through - } catch (IllegalAccessException | InvocationTargetException t) { - throw new IllegalStateException("static unmarshal(PairSeq) failed for " + type.getName(), t); + return type.cast(UNMARSHAL_PLANS.get(type).get().invoke(repr)); + } + + /* default */ static Object cachedMarshalPlan(Class runtimeType) { + return MARSHAL_PLANS.get(runtimeType).get(); + } + + /* default */ static Object cachedUnmarshalPlan(Class runtimeType) { + return UNMARSHAL_PLANS.get(runtimeType).get(); + } + + /* default */ static int marshalResolutionCount(Class runtimeType) { + return MARSHAL_PLANS.get(runtimeType).resolutionCount(); + } + + /* default */ static int unmarshalResolutionCount(Class runtimeType) { + return UNMARSHAL_PLANS.get(runtimeType).resolutionCount(); + } + + /** + * Once-only lazy plan resolver stored as the canonical {@link ClassValue} + * value. + * + * @param

                  plan type + */ + private static final class PlanHolder

                  { + private final AtomicReference

                  plan = new AtomicReference<>(); + private final ReentrantLock resolutionLock = new ReentrantLock(); + private Supplier

                  resolver; + private int resolutionCount; + + private PlanHolder(Supplier

                  resolver) { + this.resolver = resolver; } - // Or constructor T(PairSeq) - try { - Constructor c = type.getConstructor(PairSeq.class); - return c.newInstance(repr); - } catch (NoSuchMethodException e) { - throw new IllegalStateException(type.getName() + " must provide static unmarshal(PairSeq) or ctor(PairSeq)", - e); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException - | InstantiationException t) { - throw new IllegalStateException("ctor(PairSeq) failed for " + type.getName(), t); + + private P get() { + P resolved = plan.get(); + if (resolved == null) { + resolved = resolveOnce(); + } + return resolved; + } + + private P resolveOnce() { + resolutionLock.lock(); + try { + P resolved = plan.get(); + if (resolved == null) { + resolutionCount++; + resolved = resolver.get(); + resolver = null; + plan.set(resolved); + } + return resolved; + } finally { + resolutionLock.unlock(); + } + } + + private int resolutionCount() { + resolutionLock.lock(); + try { + return resolutionCount; + } finally { + resolutionLock.unlock(); + } + } + } + + /** + * Cached success or structural failure for one marshal runtime class. + */ + private static final class MarshalPlan { + private final MethodHandle handle; + private final String failureMessage; + private final Throwable failureCause; + + private MarshalPlan(MethodHandle handle, String failureMessage, Throwable failureCause) { + this.handle = handle; + this.failureMessage = failureMessage; + this.failureCause = failureCause; + } + + private static MarshalPlan resolve(Class runtimeType) { + try { + Method method = runtimeType.getMethod("marshal"); + if (!PairSeq.class.isAssignableFrom(method.getReturnType())) { + return new MarshalPlan(null, + "marshal() must return PairSeq in " + runtimeType.getName(), null); + } + MethodHandle handle = MethodHandles.lookup().unreflect(method); + return new MarshalPlan(handle, null, null); + } catch (NoSuchMethodException exception) { + return new MarshalPlan(null, runtimeType.getName() + " must implement marshal():PairSeq", exception); + } catch (IllegalAccessException exception) { + return new MarshalPlan(null, "marshal() failed for " + runtimeType.getName(), exception); + } + } + + @SuppressWarnings("PMD.AvoidCatchingGenericException") + private PairSeq invoke(Object value) { + if (handle == null) { + throw new IllegalStateException(failureMessage, failureCause); + } + try { + return (PairSeq) handle.invoke(value); + } catch (Throwable failure) { + throw new IllegalStateException("marshal() failed for " + value.getClass().getName(), + new InvocationTargetException(failure)); + } + } + } + + /** + * Cached success or structural failure for one unmarshal runtime class. + */ + private static final class UnmarshalPlan { + private final MethodHandle handle; + private final String invocationFailureMessage; + private final String resolutionFailureMessage; + private final Throwable resolutionFailureCause; + + private UnmarshalPlan(MethodHandle handle, String invocationFailureMessage, String resolutionFailureMessage, + Throwable resolutionFailureCause) { + this.handle = handle; + this.invocationFailureMessage = invocationFailureMessage; + this.resolutionFailureMessage = resolutionFailureMessage; + this.resolutionFailureCause = resolutionFailureCause; + } + + private static UnmarshalPlan resolve(Class runtimeType) { + try { + Method method = runtimeType.getMethod("unmarshal", PairSeq.class); + if (Modifier.isStatic(method.getModifiers())) { + Class returnType = method.getReturnType(); + if (!runtimeType.isAssignableFrom(returnType) && !returnType.isAssignableFrom(runtimeType)) { + return new UnmarshalPlan(null, null, + "static unmarshal(PairSeq) must return " + runtimeType.getName(), null); + } + MethodHandle handle = MethodHandles.lookup().unreflect(method); + return new UnmarshalPlan(handle, + "static unmarshal(PairSeq) failed for " + runtimeType.getName(), null, null); + } + } catch (NoSuchMethodException ignored) { + // Resolve the constructor fallback below. + } catch (IllegalAccessException exception) { + return new UnmarshalPlan(null, null, + "static unmarshal(PairSeq) failed for " + runtimeType.getName(), exception); + } + + try { + MethodHandle handle = MethodHandles.lookup() + .unreflectConstructor(runtimeType.getConstructor(PairSeq.class)); + return new UnmarshalPlan(handle, "ctor(PairSeq) failed for " + runtimeType.getName(), null, null); + } catch (NoSuchMethodException exception) { + return new UnmarshalPlan(null, null, + runtimeType.getName() + " must provide static unmarshal(PairSeq) or ctor(PairSeq)", exception); + } catch (IllegalAccessException exception) { + return new UnmarshalPlan(null, null, "ctor(PairSeq) failed for " + runtimeType.getName(), exception); + } + } + + @SuppressWarnings("PMD.AvoidCatchingGenericException") + private Object invoke(PairSeq representation) { + if (handle == null) { + throw new IllegalStateException(resolutionFailureMessage, resolutionFailureCause); + } + try { + return handle.invoke(representation); + } catch (Throwable failure) { + throw new IllegalStateException(invocationFailureMessage, new InvocationTargetException(failure)); + } } } } diff --git a/lib/src/main/java/zeroecho/core/policy/CryptoPolicy.java b/lib/src/main/java/zeroecho/core/policy/CryptoPolicy.java index 8c0f6b9..52a9487 100644 --- a/lib/src/main/java/zeroecho/core/policy/CryptoPolicy.java +++ b/lib/src/main/java/zeroecho/core/policy/CryptoPolicy.java @@ -59,16 +59,17 @@ import zeroecho.core.spec.ContextSpec; *

                * *

                Typical usage

                {@code
                - * // Install a global minimum-strength policy
                - * CryptoAlgorithms.setPolicy(CryptoPolicy.minStrength(128));
                + * ZeroEchoSession session = new ZeroEchoSession()
                + *         .withPolicy(CryptoPolicy.minStrength(128));
                  *
                  * // Later, when creating a context:
                - * EncryptionContext ctx = CryptoAlgorithms.create("AES/GCM", KeyUsage.ENCRYPT, secretKey);
                + * EncryptionContext ctx = session.createContext("AES", KeyUsage.ENCRYPT, secretKey);
                  * }
                * * @param context specification type * @param key type * @since 1.0 + * @see zeroecho.sdk.ZeroEchoSession */ public interface CryptoPolicy { // NOPMD /** diff --git a/lib/src/main/java/zeroecho/core/policy/package-info.java b/lib/src/main/java/zeroecho/core/policy/package-info.java index ca64411..c589f91 100644 --- a/lib/src/main/java/zeroecho/core/policy/package-info.java +++ b/lib/src/main/java/zeroecho/core/policy/package-info.java @@ -52,13 +52,12 @@ *
              * *

              Typical usage

              {@code
              - * // Install a global minimum-strength policy.
              - * zeroecho.core.CryptoAlgorithms.setPolicy(
              - *     zeroecho.core.policy.CryptoPolicy.minStrength(128));
              + * zeroecho.sdk.ZeroEchoSession session = new zeroecho.sdk.ZeroEchoSession()
              + *     .withPolicy(zeroecho.core.policy.CryptoPolicy.minStrength(128));
                *
              - * // Later, when creating a context, the policy is consulted automatically.
              + * // The session applies the policy without changing other consumers.
                * zeroecho.core.context.EncryptionContext ctx =
              - *     zeroecho.core.CryptoAlgorithms.create("AES/GCM", zeroecho.core.KeyUsage.ENCRYPT, secretKey);
              + *     session.createContext("AES", zeroecho.core.KeyUsage.ENCRYPT, secretKey);
                * }
              * *

              Design notes

              diff --git a/lib/src/main/java/zeroecho/core/spec/AlgorithmKeySpec.java b/lib/src/main/java/zeroecho/core/spec/AlgorithmKeySpec.java index e7d8875..c7eaf76 100644 --- a/lib/src/main/java/zeroecho/core/spec/AlgorithmKeySpec.java +++ b/lib/src/main/java/zeroecho/core/spec/AlgorithmKeySpec.java @@ -33,7 +33,6 @@ ******************************************************************************/ package zeroecho.core.spec; -import zeroecho.core.spi.SymmetricKeyBuilder; /** * Marker interface for algorithm-specific key specifications. @@ -46,7 +45,7 @@ import zeroecho.core.spi.SymmetricKeyBuilder; *

              Design

              *
                *
              • Separates algorithm parameters from key builders such as - * {@link SymmetricKeyBuilder}.
              • + * operation-specific key generator or importer. *
              • Provides a type-safe way to pass algorithm requirements around instead of * raw integers or opaque byte arrays.
              • *
              • Allows higher layers to work generically with {@code AlgorithmKeySpec} diff --git a/lib/src/main/java/zeroecho/core/spec/VoidSpec.java b/lib/src/main/java/zeroecho/core/spec/VoidSpec.java index aa264c4..256ca51 100644 --- a/lib/src/main/java/zeroecho/core/spec/VoidSpec.java +++ b/lib/src/main/java/zeroecho/core/spec/VoidSpec.java @@ -43,7 +43,7 @@ package zeroecho.core.spec; *

                * *

                Usage

                {@code
                - * CryptoContext ctx = algo.create(KeyUsage.SIGN, privateKey, VoidSpec.INSTANCE);
                + * CryptoContext ctx = algo.createContext(KeyUsage.SIGN, privateKey, VoidSpec.INSTANCE);
                  * }
                * *

                diff --git a/lib/src/main/java/zeroecho/core/spec/package-info.java b/lib/src/main/java/zeroecho/core/spec/package-info.java index 7dd5f1a..4f780f1 100644 --- a/lib/src/main/java/zeroecho/core/spec/package-info.java +++ b/lib/src/main/java/zeroecho/core/spec/package-info.java @@ -56,12 +56,12 @@ *

                Typical usage

                {@code
                  * // Algorithm requires no per-operation parameters.
                  * zeroecho.core.context.CryptoContext ctx =
                - *     algo.create(zeroecho.core.KeyUsage.SIGN, privateKey, zeroecho.core.spec.VoidSpec.INSTANCE);
                + *     algo.createContext(zeroecho.core.KeyUsage.SIGN, privateKey, zeroecho.core.spec.VoidSpec.INSTANCE);
                  *
                  * // Algorithm with parameters: pass an algorithm-specific ContextSpec implementation.
                  * // Example: RSA with OAEP/PSS, AEAD tag length, etc.
                  * // zeroecho.core.context.CryptoContext ctx =
                - * //     algo.create(zeroecho.core.KeyUsage.ENCRYPT, key, someAlgorithmSpecificSpec);
                + * //     algo.createContext(zeroecho.core.KeyUsage.ENCRYPT, key, someAlgorithmSpecificSpec);
                  * }
                * *

                Design notes

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

                - * Implementations encapsulate algorithm-specific details (for example RSA, - * Ed25519, X25519) while exposing a uniform API for generation and import - * operations. This allows higher-level code to work generically with - * {@code AsymmetricKeyBuilder} without depending on algorithm internals. - *

                - * - *

                Operations

                - *
                  - *
                • {@link #generateKeyPair(AlgorithmKeySpec)} - creates a fresh key pair - * using the supplied algorithm spec (such as curve parameters or modulus - * length).
                • - *
                • {@link #importPublic(AlgorithmKeySpec)} - wraps externally supplied - * public key material in a {@link PublicKey}, validating that it conforms to - * the algorithm specification.
                • - *
                • {@link #importPrivate(AlgorithmKeySpec)} - wraps externally supplied - * private key material in a {@link PrivateKey}, validating that it conforms to - * the algorithm specification.
                • - *
                - * - *

                Usage guidelines

                - *
                  - *
                • Always prefer {@link #generateKeyPair(AlgorithmKeySpec)} when creating - * new credentials.
                • - *
                • Use {@link #importPublic(AlgorithmKeySpec)} and - * {@link #importPrivate(AlgorithmKeySpec)} for interoperability, loading from - * key stores, or migration from existing material.
                • - *
                • Implementations should reject malformed or weak keys and enforce - * algorithm-specific constraints (for example, minimum modulus length for RSA - * or disallowed small subgroup curves).
                • - *
                - * - *

                Thread safety

                Implementations must be stateless or otherwise safe - * for concurrent use across threads. - * - * @param algorithm-specific key specification type - * - * @since 1.0 - */ -public interface AsymmetricKeyBuilder { - /** - * Generates a new asymmetric key pair according to the given specification. - * - * @param spec algorithm parameters, such as modulus length or curve identifier - * @return a new {@link KeyPair} containing a public and private key - * @throws GeneralSecurityException if the algorithm or parameters are invalid - * or unsupported - */ - KeyPair generateKeyPair(S spec) throws GeneralSecurityException; - - /** - * Imports an externally supplied public key according to the given - * specification. - * - *

                - * Implementations must validate that the provided material is properly - * formatted, has acceptable length, and is consistent with the specified - * algorithm. - *

                - * - * @param spec algorithm parameters and encoded public key material - * @return a {@link PublicKey} validated and usable for cryptographic operations - * @throws GeneralSecurityException if the key material is invalid or does not - * match the specification - */ - PublicKey importPublic(S spec) throws GeneralSecurityException; - - /** - * Imports an externally supplied private key according to the given - * specification. - * - *

                - * Implementations must validate that the provided material is properly - * formatted, has acceptable length, and is consistent with the specified - * algorithm. - *

                - * - * @param spec algorithm parameters and encoded private key material - * @return a {@link PrivateKey} validated and usable for cryptographic - * operations - * @throws GeneralSecurityException if the key material is invalid or does not - * match the specification - */ - PrivateKey importPrivate(S spec) throws GeneralSecurityException; -} diff --git a/lib/src/main/java/zeroecho/core/spi/AsymmetricKeyPairGenerator.java b/lib/src/main/java/zeroecho/core/spi/AsymmetricKeyPairGenerator.java new file mode 100644 index 0000000..804d81d --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spi/AsymmetricKeyPairGenerator.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core.spi; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; + +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Generates asymmetric key pairs for one exact specification type. + * Implementations must be stateless or otherwise safe for concurrent invocation. + * + * @param specification type + * @since 1.0 + */ +@FunctionalInterface +public interface AsymmetricKeyPairGenerator { + /** + * Generates a key pair. + * + * @param spec generation parameters + * @return generated key pair + * @throws GeneralSecurityException if generation fails + */ + KeyPair generateKeyPair(S spec) throws GeneralSecurityException; +} diff --git a/lib/src/main/java/zeroecho/core/spi/ContextConstructorKS.java b/lib/src/main/java/zeroecho/core/spi/ContextConstructorKS.java deleted file mode 100644 index e5854b3..0000000 --- a/lib/src/main/java/zeroecho/core/spi/ContextConstructorKS.java +++ /dev/null @@ -1,96 +0,0 @@ -/******************************************************************************* - * Copyright (C) 2026, Leo Galambos - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software must - * display the following acknowledgement: - * This product includes software developed by the Egothor project. - * - * 4. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ******************************************************************************/ -package zeroecho.core.spi; - -import java.io.IOException; -import java.security.Key; - -import zeroecho.core.CryptoAlgorithm; -import zeroecho.core.context.CryptoContext; -import zeroecho.core.spec.ContextSpec; - -/** - * Factory interface to construct a {@link CryptoContext} from a key and an - * optional specification. - * - *

                - * Each cryptographic algorithm binds one or more roles (for example, - * {@code ENCRYPT}, {@code SIGN}) to a corresponding context type. For each - * binding, a {@code ContextConstructorKS} is registered as the factory that - * creates the runtime context for the role. The role itself is implied by the - * registration and does not need to be passed explicitly here. - *

                - * - *

                Responsibilities

                - *
                  - *
                • Validate that the provided key is compatible with the expected key - * type.
                • - *
                • Interpret the {@link ContextSpec} parameters (such as IVs, padding modes, - * or curve identifiers).
                • - *
                • Construct and return a ready-to-use {@link CryptoContext} instance bound - * to the given key and spec.
                • - *
                - * - *

                Usage

                - *

                - * {@code ContextConstructorKS} is primarily used internally by - * {@link CryptoAlgorithm} implementations when binding roles. Higher-level code - * should not call it directly; instead use {@link CryptoAlgorithm#create} or - * {@link zeroecho.core.CryptoAlgorithms#create}. - *

                - * - *

                Thread safety

                Implementations should be stateless and safe to invoke - * concurrently from multiple threads. - * - * @param context type produced - * @param key type accepted - * @param specification type accepted - * - * @since 1.0 - */ -@FunctionalInterface -public interface ContextConstructorKS { - /** - * Creates a new {@link CryptoContext} instance bound to the provided key and - * specification. - * - * @param key non-null cryptographic key suitable for the role - * @param spec role-specific parameters; may be {@code null} if defaults are - * acceptable - * @return a newly constructed context ready for cryptographic operations - * @throws IOException if context creation fails due to I/O, provider issues, or - * invalid parameters - */ - C create(K key, S spec) throws IOException; -} diff --git a/lib/src/main/java/zeroecho/core/spi/ContextFactoryKS.java b/lib/src/main/java/zeroecho/core/spi/ContextFactoryKS.java new file mode 100644 index 0000000..2af71e3 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spi/ContextFactoryKS.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the conditions in the project LICENSE are met. + ******************************************************************************/ +package zeroecho.core.spi; + +import java.security.Key; + +import zeroecho.core.context.CryptoContext; +import zeroecho.core.spec.ContextSpec; + +/** + * Creates a cryptographic context from a key and a context specification. + * + *

                Implementations report provider and parameter failures with unchecked + * exceptions; context construction is a pure in-memory operation and does not + * expose an I/O failure contract. Factories must be stateless or otherwise safe + * for concurrent invocation; returned contexts retain their own documented + * thread-safety contracts.

                + * + * @param context type produced + * @param key type accepted + * @param specification type accepted + * @since 1.0 + */ +@FunctionalInterface +public interface ContextFactoryKS { + /** + * Creates a context bound to the supplied key and specification. + * + * @param key non-null key + * @param spec non-null resolved context specification + * @return a newly created context + */ + C createContext(K key, S spec); +} diff --git a/lib/src/main/java/zeroecho/core/spi/PrivateKeyImporter.java b/lib/src/main/java/zeroecho/core/spi/PrivateKeyImporter.java new file mode 100644 index 0000000..6cb7c44 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spi/PrivateKeyImporter.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core.spi; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; + +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Imports private keys for one exact specification type. Implementations must be + * stateless or otherwise safe for concurrent invocation. + * + * @param specification type + * @since 1.0 + */ +@FunctionalInterface +public interface PrivateKeyImporter { + /** + * Imports a private key. + * + * @param spec encoded key material and parameters + * @return imported private key + * @throws GeneralSecurityException if validation or import fails + */ + PrivateKey importPrivate(S spec) throws GeneralSecurityException; +} diff --git a/lib/src/main/java/zeroecho/core/spi/PublicKeyImporter.java b/lib/src/main/java/zeroecho/core/spi/PublicKeyImporter.java new file mode 100644 index 0000000..3c90dbb --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spi/PublicKeyImporter.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core.spi; + +import java.security.GeneralSecurityException; +import java.security.PublicKey; + +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Imports public keys for one exact specification type. Implementations must be + * stateless or otherwise safe for concurrent invocation. + * + * @param specification type + * @since 1.0 + */ +@FunctionalInterface +public interface PublicKeyImporter { + /** + * Imports a public key. + * + * @param spec encoded key material and parameters + * @return imported public key + * @throws GeneralSecurityException if validation or import fails + */ + PublicKey importPublic(S spec) throws GeneralSecurityException; +} diff --git a/lib/src/main/java/zeroecho/core/spi/SymmetricKeyBuilder.java b/lib/src/main/java/zeroecho/core/spi/SymmetricKeyBuilder.java deleted file mode 100644 index 53c91dd..0000000 --- a/lib/src/main/java/zeroecho/core/spi/SymmetricKeyBuilder.java +++ /dev/null @@ -1,114 +0,0 @@ -/******************************************************************************* - * Copyright (C) 2026, Leo Galambos - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software must - * display the following acknowledgement: - * This product includes software developed by the Egothor project. - * - * 4. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ******************************************************************************/ -package zeroecho.core.spi; - -import java.security.GeneralSecurityException; - -import javax.crypto.SecretKey; - -import zeroecho.core.spec.AlgorithmKeySpec; - -/** - * Factory interface for constructing symmetric keys from algorithm-specific - * specifications. - * - *

                - * Implementations encapsulate the details of generating or importing - * {@link SecretKey} instances for a particular symmetric algorithm (for example - * AES, ChaCha20, or HMAC). This abstraction provides a uniform API for higher - * layers, independent of provider-specific implementations. - *

                - * - *

                Operations

                - *
                  - *
                • {@link #generateSecret(AlgorithmKeySpec)} - creates a fresh random key - * using the parameters supplied by the algorithm specification.
                • - *
                • {@link #importSecret(AlgorithmKeySpec)} - wraps externally supplied raw - * key material in a {@link SecretKey}, validating that it conforms to the - * specification.
                • - *
                - * - *

                Usage guidelines

                - *
                  - *
                • Prefer {@link #generateSecret(AlgorithmKeySpec)} for new credentials to - * ensure strong, random keys.
                • - *
                • Use {@link #importSecret(AlgorithmKeySpec)} only when loading existing - * keys, migrating from another system, or interoperating with external storage - * formats.
                • - *
                • Implementations must enforce algorithm constraints, including required - * key sizes and disallowing known-weak parameters.
                • - *
                • Returned {@link SecretKey} instances should be immutable and, where - * possible, wrapped in provider-specific classes that prevent serialization or - * unintended exposure.
                • - *
                - * - *

                Thread safety

                - *

                - * Implementations must be stateless or otherwise safe to use concurrently - * across multiple threads. - *

                - * - * @param algorithm-specific key specification type - * - * @since 1.0 - */ -public interface SymmetricKeyBuilder { - /** - * Generates a new symmetric key according to the given specification. - * - * @param spec algorithm parameters, such as required key size or algorithm - * variant - * @return a freshly generated {@link SecretKey} containing random key material - * @throws GeneralSecurityException if key generation fails or the parameters - * are invalid or unsupported - */ - SecretKey generateSecret(S spec) throws GeneralSecurityException; - - /** - * Imports an externally provided symmetric key according to the given - * specification. - * - *

                - * Implementations must validate that the provided material matches the - * algorithm’s requirements (for example, correct length and encoding). Weak or - * truncated keys must be rejected. - *

                - * - * @param spec algorithm parameters and raw key material - * @return a validated {@link SecretKey} suitable for cryptographic use - * @throws GeneralSecurityException if the key material is invalid, corrupted, - * or inconsistent with the specification - */ - SecretKey importSecret(S spec) throws GeneralSecurityException; -} diff --git a/lib/src/main/java/zeroecho/core/spi/SymmetricKeyGenerator.java b/lib/src/main/java/zeroecho/core/spi/SymmetricKeyGenerator.java new file mode 100644 index 0000000..2340324 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spi/SymmetricKeyGenerator.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core.spi; + +import java.security.GeneralSecurityException; + +import javax.crypto.SecretKey; + +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Generates symmetric keys for one exact specification type. Implementations + * must be stateless or otherwise safe for concurrent invocation. + * + * @param specification type + * @since 1.0 + */ +@FunctionalInterface +public interface SymmetricKeyGenerator { + /** + * Generates a symmetric key. + * + * @param spec generation parameters + * @return generated key + * @throws GeneralSecurityException if generation fails + */ + SecretKey generateSecret(S spec) throws GeneralSecurityException; +} diff --git a/lib/src/main/java/zeroecho/core/spi/SymmetricKeyImporter.java b/lib/src/main/java/zeroecho/core/spi/SymmetricKeyImporter.java new file mode 100644 index 0000000..d7ebb32 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spi/SymmetricKeyImporter.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core.spi; + +import java.security.GeneralSecurityException; + +import javax.crypto.SecretKey; + +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Imports symmetric keys for one exact specification type. Implementations must + * be stateless or otherwise safe for concurrent invocation. + * + * @param specification type + * @since 1.0 + */ +@FunctionalInterface +public interface SymmetricKeyImporter { + /** + * Imports a symmetric key. + * + * @param spec encoded key material and parameters + * @return imported key + * @throws GeneralSecurityException if validation or import fails + */ + SecretKey importSecret(S spec) throws GeneralSecurityException; +} diff --git a/lib/src/main/java/zeroecho/core/spi/package-info.java b/lib/src/main/java/zeroecho/core/spi/package-info.java index fc23fb1..896d4bf 100644 --- a/lib/src/main/java/zeroecho/core/spi/package-info.java +++ b/lib/src/main/java/zeroecho/core/spi/package-info.java @@ -32,152 +32,22 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ******************************************************************************/ /** - * Service Provider Interfaces (SPI) for extending ZeroEcho with custom - * cryptographic algorithms and key builders. + * Provider contracts for context construction and exact key operations. * - *

                - * This package defines the provider-facing contracts used by algorithms to plug - * new primitives into the framework while keeping the public API uniform. The - * SPIs emphasize role-driven context construction, strict spec validation, and - * safe key lifecycle handling. - *

                + *

                Algorithms bind each supported role to a {@link ContextFactoryKS}. Context + * construction is an in-memory operation; stream attachment and processing are + * responsible for reporting {@link java.io.IOException}.

                * - *

                How algorithms plug in

                - *

                - * An algorithm publishes capabilities and binds each supported - * {@link zeroecho.core.KeyUsage role} to a factory that creates a matching - * {@link zeroecho.core.context.CryptoContext}. The binding is registered in the - * algorithm constructor using {@link ContextConstructorKS}; the role is implied - * by the binding itself. - *

                + *

                Key capabilities are registered independently through + * {@link SymmetricKeyGenerator}, {@link SymmetricKeyImporter}, + * {@link AsymmetricKeyPairGenerator}, {@link PublicKeyImporter}, and + * {@link PrivateKeyImporter}. A provider registers only the operations it + * implements, so capability lookup fails before invocation instead of returning + * an object with unsupported methods.

                * - *
                {@code
                - * // Inside an algorithm's constructor (illustrative):
                - * capability(AlgorithmFamily.SYMMETRIC, KeyUsage.ENCRYPT,
                - *           zeroecho.core.context.EncryptionContext.class,
                - *           javax.crypto.SecretKey.class, zeroecho.core.alg.aes.AesSpec.class,
                - *           (k, s) -> new zeroecho.core.alg.aes.AesCipherContext(this, k, true, s, new java.security.SecureRandom()),
                - *           () -> zeroecho.core.alg.aes.AesSpec.gcm128(null));
                - *
                - * capability(AlgorithmFamily.SYMMETRIC, KeyUsage.DECRYPT,
                - *           zeroecho.core.context.EncryptionContext.class,
                - *           javax.crypto.SecretKey.class, zeroecho.core.alg.aes.AesSpec.class,
                - *           (k, s) -> new zeroecho.core.alg.aes.AesCipherContext(this, k, false, s, new java.security.SecureRandom()),
                - *           () -> zeroecho.core.alg.aes.AesSpec.gcm128(null));
                - * }
                - * - *

                Key material builders and the spec-class keyed registry

                - *

                - * Builders are registered per spec class. The algorithm maintains a single map - * from {@code Class} to a - * builder instance; lookups are driven by the spec class, not by the high-level - * operation. This keeps registration simple while letting providers model - * different intents through different spec types. - *

                - * - *
                  - *
                • {@link AsymmetricKeyBuilder} provides key pair generation and - * public/private key import for asymmetric algorithms.
                • - *
                • {@link SymmetricKeyBuilder} provides key generation and key import for - * {@link javax.crypto.SecretKey}-based algorithms.
                • - *
                - * - *

                Why a single interface for both key pair generation and key import

                - *

                - * There is one cohesive builder interface because the registry keys off the - * spec class, not the operation. A single builder type per spec class gives one - * lookup path and one place to enforce spec parsing, format checks, curve or - * modulus validation, and provider constraints. The public facades in - * {@link zeroecho.core.CryptoAlgorithm} and - * {@link zeroecho.core.CryptoAlgorithms} remain compact: they resolve the - * builder by spec class and then invoke - * {@link AsymmetricKeyBuilder#generateKeyPair(zeroecho.core.spec.AlgorithmKeySpec)}, - * {@link AsymmetricKeyBuilder#importPublic(zeroecho.core.spec.AlgorithmKeySpec)}, - * {@link AsymmetricKeyBuilder#importPrivate(zeroecho.core.spec.AlgorithmKeySpec)}, - * {@link SymmetricKeyBuilder#generateSecret(zeroecho.core.spec.AlgorithmKeySpec)}, - * or - * {@link SymmetricKeyBuilder#importSecret(zeroecho.core.spec.AlgorithmKeySpec)} - * as appropriate. - *

                - *

                - * Providers express differences in capability by choosing distinct spec classes - * rather than multiplying interfaces. For example, a symmetric provider may - * register {@code AesKeyGenSpec} for generation and {@code AesKeyImportSpec} - * for import, each with its own builder. The unified interface still fits - * because the registry discriminates by spec class. - *

                - * - *

                Pattern: separate specs for generation vs import with prescriptive - * exceptions

                - *

                - * It is idiomatic to register two builders for AES: one bound to a generation - * spec that implements - * {@link SymmetricKeyBuilder#generateSecret(zeroecho.core.spec.AlgorithmKeySpec)} - * and rejects import, and one bound to an import spec that implements - * {@link SymmetricKeyBuilder#importSecret(zeroecho.core.spec.AlgorithmKeySpec)} - * and rejects generation. Unsupported paths should throw - * {@link UnsupportedOperationException} with a clear, prescriptive message that - * points to the correct spec. - *

                - * - *
                {@code
                - * // Generation-only builder bound to AesKeyGenSpec
                - * registerSymmetricKeyBuilder(zeroecho.core.alg.aes.AesKeyGenSpec.class, new SymmetricKeyBuilder<>() {
                - *   @Override public javax.crypto.SecretKey generateSecret(zeroecho.core.alg.aes.AesKeyGenSpec spec)
                - *       throws java.security.GeneralSecurityException {
                - *     // generate according to spec.keySizeBits()
                - *     throw new UnsupportedOperationException("example");
                - *   }
                - *   @Override public javax.crypto.SecretKey importSecret(zeroecho.core.alg.aes.AesKeyGenSpec spec) {
                - *     throw new UnsupportedOperationException("Use AesKeyImportSpec for importing AES keys");
                - *   }
                - * }, zeroecho.core.alg.aes.AesKeyGenSpec::aes256);
                - *
                - * // Import-only builder bound to AesKeyImportSpec
                - * registerSymmetricKeyBuilder(zeroecho.core.alg.aes.AesKeyImportSpec.class, new SymmetricKeyBuilder<>() {
                - *   @Override public javax.crypto.SecretKey generateSecret(zeroecho.core.alg.aes.AesKeyImportSpec spec) {
                - *     throw new UnsupportedOperationException("Use AesKeyGenSpec to generate AES keys");
                - *   }
                - *   @Override public javax.crypto.SecretKey importSecret(zeroecho.core.alg.aes.AesKeyImportSpec spec) {
                - *     return new javax.crypto.spec.SecretKeySpec(spec.key(), "AES");
                - *   }
                - * }, null);
                - * }
                - * - *

                - * The same separation can be applied to asymmetric algorithms by using, for - * example, {@code RsaKeyGenSpec} and {@code RsaKeyImportSpec}. Each spec class - * maps to exactly one {@link AsymmetricKeyBuilder} in the algorithm's registry. - *

                - * - *

                Context sharing and parameter flow

                - *

                - * Contexts that need per-session values (IV, nonce, salt, AAD) may implement - * {@link ContextAware} to read and write through a shared - * {@link conflux.CtxInterface}. For symmetric streams that carry lightweight - * headers, algorithms in {@code zeroecho.core} can use - * {@link zeroecho.core.SymmetricHeaderCodec}. - *

                - * - *

                Error handling and validation

                - *
                  - *
                • Fail fast when keys or specs are incompatible with the bound role.
                • - *
                • Use {@link java.security.GeneralSecurityException} for key generation or - * import failures.
                • - *
                • Use {@link java.io.IOException} from - * {@link ContextConstructorKS#create(java.security.Key, zeroecho.core.spec.ContextSpec)} - * when context setup performs I/O.
                • - *
                • Use {@link UnsupportedOperationException} with a precise message when an - * operation is intentionally unsupported by the spec or provider, for example - * "Use AesKeyImportSpec for importing AES keys".
                • - *
                - * - *

                Thread safety

                - *

                - * SPI implementations should be stateless or otherwise safe for concurrent use. - * Created contexts are not necessarily thread-safe unless explicitly documented - * by the provider. - *

                + *

                SPI implementations should be stateless or otherwise safe for concurrent + * lookup and invocation. Created cryptographic contexts remain operation-local + * and are not necessarily thread-safe.

                * * @since 1.0 */ diff --git a/lib/src/main/java/zeroecho/core/storage/KeyringStore.java b/lib/src/main/java/zeroecho/core/storage/KeyringStore.java index 6b91b88..cadd102 100644 --- a/lib/src/main/java/zeroecho/core/storage/KeyringStore.java +++ b/lib/src/main/java/zeroecho/core/storage/KeyringStore.java @@ -52,12 +52,18 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import javax.crypto.SecretKey; +import javax.security.auth.DestroyFailedException; +import javax.security.auth.Destroyable; -import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.KeyOperation; +import zeroecho.core.KeyOperationInfo; import zeroecho.core.marshal.PairSeq; import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.sdk.ZeroEchoSession; /** * Human-editable keyring persisted in a simple UTF-8 text format. @@ -82,7 +88,8 @@ import zeroecho.core.spec.AlgorithmKeySpec; * }
      * *

      Reading and writing

      Use {@link #save(java.nio.file.Path)} to write - * the keyring to disk and {@link #load(java.nio.file.Path)} to read it back. + * the keyring to disk and {@link #load(ZeroEchoSession, java.nio.file.Path)} to + * read it back. * The loader tolerates the presence of the header and comment lines but * requires the magic header for the v1 format. * @@ -95,15 +102,16 @@ import zeroecho.core.spec.AlgorithmKeySpec; * * These are discovered and invoked via reflection. See * {@link #marshalSpec(AlgorithmKeySpec)} and - * {@link #unmarshalSpec(String, PairSeq)} for details. + * {@link #unmarshalSpec(Class, PairSeq)} for details. * *

      Basic usage

      {@code
      - * KeyringStore ks = new KeyringStore();
      + * ZeroEchoSession session = new ZeroEchoSession();
      + * KeyringStore ks = new KeyringStore(session);
        * ks.putPublic("site-signing", "Ed25519", myEd25519PublicSpec);
        * ks.putPrivate("site-signing", "Ed25519", myEd25519PrivateSpec);
        * ks.save(Path.of("keyring.txt"));
        *
      - * KeyringStore reloaded = KeyringStore.load(Path.of("keyring.txt"));
      + * KeyringStore reloaded = KeyringStore.load(session, Path.of("keyring.txt"));
        * PublicKey pub = reloaded.getPublic("site-signing");
        * }
      */ @@ -120,6 +128,17 @@ public final class KeyringStore { // NOPMD private static final String SUFFIX_PRIVATE = ".priv"; private final Map byAlias = new LinkedHashMap<>(); + private final ZeroEchoSession session; + + /** + * Creates an empty keyring bound to a runtime session. + * + * @param session explicit runtime configuration + * @throws NullPointerException if {@code session} is {@code null} + */ + public KeyringStore(ZeroEchoSession session) { + this.session = Objects.requireNonNull(session, "session must not be null"); + } /** * Immutable entry in a {@link KeyringStore}. @@ -228,7 +247,7 @@ public final class KeyringStore { // NOPMD * PublicWithId pairs the algorithm identifier with a resolved public key. * *

      Usage

      {@code
      -     * KeyringStore ks = KeyringStore.load(path);
      +     * KeyringStore ks = KeyringStore.load(session, path);
            * KeyringStore.PublicWithId r = ks.getPublicWithId("alice");
            * String algId = r.algorithm();
            * PublicKey pub = r.key();
      @@ -241,7 +260,7 @@ public final class KeyringStore { // NOPMD
            * PrivateWithId pairs the algorithm identifier with a resolved private key.
            *
            * 

      Usage

      {@code
      -     * KeyringStore ks = KeyringStore.load(path);
      +     * KeyringStore ks = KeyringStore.load(session, path);
            * KeyringStore.PrivateWithId r = ks.getPrivateWithId("alice");
            * String algId = r.algorithm();
            * PrivateKey prv = r.key();
      @@ -254,7 +273,7 @@ public final class KeyringStore { // NOPMD
            * SecretWithId pairs the algorithm identifier with a resolved secret key.
            *
            * 

      Usage

      {@code
      -     * KeyringStore ks = KeyringStore.load(path);
      +     * KeyringStore ks = KeyringStore.load(session, path);
            * KeyringStore.SecretWithId r = ks.getSecretWithId("hmac-key");
            * String algId = r.algorithm();
            * SecretKey sk = r.key();
      @@ -280,8 +299,8 @@ public final class KeyringStore { // NOPMD
            */
           public PublicWithId getPublicWithId(String alias) throws GeneralSecurityException {
               Record r = require(withPublicSuffix(alias), Record.Kind.PUBLIC_KEY);
      -        AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload);
      -        PublicKey key = CryptoAlgorithms.publicKey(r.algorithm, spec);
      +        AlgorithmKeySpec spec = unmarshalRecord(r);
      +        PublicKey key = session.keyBuilders().asymmetric().importPublic(r.algorithm, spec);
               return new PublicWithId(r.algorithm, key);
           }
       
      @@ -296,9 +315,17 @@ public final class KeyringStore { // NOPMD
            */
           public PrivateWithId getPrivateWithId(String alias) throws GeneralSecurityException {
               Record r = require(withPrivateSuffix(alias), Record.Kind.PRIVATE_KEY);
      -        AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload);
      -        PrivateKey key = CryptoAlgorithms.privateKey(r.algorithm, spec);
      -        return new PrivateWithId(r.algorithm, key);
      +        AlgorithmKeySpec spec = unmarshalRecord(r);
      +        Throwable failure = null;
      +        try {
      +            PrivateKey key = session.keyBuilders().asymmetric().importPrivate(r.algorithm, spec);
      +            return new PrivateWithId(r.algorithm, key);
      +        } catch (GeneralSecurityException | RuntimeException | Error exception) { // NOPMD - retain primary failure
      +            failure = exception;
      +            throw exception;
      +        } finally {
      +            destroyTemporarySpec(spec, failure);
      +        }
           }
       
           /**
      @@ -312,9 +339,17 @@ public final class KeyringStore { // NOPMD
            */
           public SecretWithId getSecretWithId(String alias) throws GeneralSecurityException {
               Record r = require(alias, Record.Kind.SECRET_KEY);
      -        AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload);
      -        SecretKey key = CryptoAlgorithms.secretKey(r.algorithm, spec);
      -        return new SecretWithId(r.algorithm, key);
      +        AlgorithmKeySpec spec = unmarshalRecord(r);
      +        Throwable failure = null;
      +        try {
      +            SecretKey key = session.keyBuilders().symmetric().importKey(r.algorithm, spec);
      +            return new SecretWithId(r.algorithm, key);
      +        } catch (GeneralSecurityException | RuntimeException | Error exception) { // NOPMD - retain primary failure
      +            failure = exception;
      +            throw exception;
      +        } finally {
      +            destroyTemporarySpec(spec, failure);
      +        }
           }
       
           /**
      @@ -322,7 +357,7 @@ public final class KeyringStore { // NOPMD
            *
            * 

      * The stored spec class is loaded and unmarshaled via - * {@link #unmarshalSpec(String, PairSeq)}, and the key is materialized via the + * {@link #unmarshalSpec(Class, PairSeq)}, and the key is materialized via the * crypto catalog. *

      * @@ -335,8 +370,8 @@ public final class KeyringStore { // NOPMD */ public PublicKey getPublic(String alias) throws GeneralSecurityException { Record r = require(withPublicSuffix(alias), Record.Kind.PUBLIC_KEY); - AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload); - return CryptoAlgorithms.publicKey(r.algorithm, spec); + AlgorithmKeySpec spec = unmarshalRecord(r); + return session.keyBuilders().asymmetric().importPublic(r.algorithm, spec); } /** @@ -351,8 +386,16 @@ public final class KeyringStore { // NOPMD */ public PrivateKey getPrivate(String alias) throws GeneralSecurityException { Record r = require(withPrivateSuffix(alias), Record.Kind.PRIVATE_KEY); - AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload); - return CryptoAlgorithms.privateKey(r.algorithm, spec); + AlgorithmKeySpec spec = unmarshalRecord(r); + Throwable failure = null; + try { + return session.keyBuilders().asymmetric().importPrivate(r.algorithm, spec); + } catch (GeneralSecurityException | RuntimeException | Error exception) { // NOPMD - retain primary failure + failure = exception; + throw exception; + } finally { + destroyTemporarySpec(spec, failure); + } } private static String withPublicSuffix(String baseAlias) { @@ -401,8 +444,16 @@ public final class KeyringStore { // NOPMD */ public SecretKey getSecret(String alias) throws GeneralSecurityException { Record r = require(alias, Record.Kind.SECRET_KEY); - AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload); - return CryptoAlgorithms.secretKey(r.algorithm, spec); + AlgorithmKeySpec spec = unmarshalRecord(r); + Throwable failure = null; + try { + return session.keyBuilders().symmetric().importKey(r.algorithm, spec); + } catch (GeneralSecurityException | RuntimeException | Error exception) { // NOPMD - retain primary failure + failure = exception; + throw exception; + } finally { + destroyTemporarySpec(spec, failure); + } } /** @@ -420,14 +471,15 @@ public final class KeyringStore { // NOPMD /** * Loads a keyring from a UTF-8 text file. * + * @param session explicit runtime configuration * @param path source path * @return a new store populated with entries from the file * @throws IOException if reading fails or the format is not supported */ - public static KeyringStore load(Path path) throws IOException { + public static KeyringStore load(ZeroEchoSession session, Path path) throws IOException { try (BufferedReader r = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { List recs = readAll(r, /* requireHeader */ true); - KeyringStore store = new KeyringStore(); + KeyringStore store = new KeyringStore(session); recs.forEach(rec -> store.byAlias.put(rec.alias, rec)); return store; } @@ -616,6 +668,7 @@ public final class KeyringStore { // NOPMD if (importSpec == null) { throw new IllegalArgumentException("importSpec"); } + registeredSpecClass(algorithmId, kind, importSpec.getClass().getName()); PairSeq payload = marshalSpec(importSpec); @@ -659,21 +712,69 @@ public final class KeyringStore { // NOPMD * Calls a static {@code unmarshal(PairSeq)} method on the spec class. * * @param spec type - * @param specClass fully qualified spec class name + * @param specClass registered specification class * @param p the pair sequence to unmarshal * @return the reconstructed spec instance * @throws IllegalStateException if reflection fails or the method is absent */ @SuppressWarnings("unchecked") - private static S unmarshalSpec(String specClass, PairSeq p) { + private static S unmarshalSpec(Class specClass, PairSeq p) { try { - Class cls = Class.forName(specClass); - Method m = cls.getMethod("unmarshal", PairSeq.class); + Method m = specClass.getMethod("unmarshal", PairSeq.class); Object out = m.invoke(null, p); return (S) out; - } catch (IllegalAccessException | InvocationTargetException | ClassNotFoundException | NoSuchMethodException - | SecurityException e) { - throw new IllegalStateException("Spec unmarshal failed for " + specClass, e); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | SecurityException e) { + throw new IllegalStateException("Spec unmarshal failed for " + specClass.getName(), e); + } + } + + private AlgorithmKeySpec unmarshalRecord(Record record) { + Class specClass = registeredSpecClass(record.algorithm, record.kind, + record.specClass); + return unmarshalSpec(specClass, record.specPayload); + } + + private Class registeredSpecClass(String algorithmId, Record.Kind kind, + String persistedClassName) { + if (persistedClassName == null || persistedClassName.isBlank()) { + throw new IllegalArgumentException("Missing key specification class"); + } + CryptoAlgorithm algorithm = session.require(algorithmId); + KeyOperation expectedOperation = switch (kind) { + case PUBLIC_KEY -> KeyOperation.ASYMMETRIC_PUBLIC_IMPORT; + case PRIVATE_KEY -> KeyOperation.ASYMMETRIC_PRIVATE_IMPORT; + case SECRET_KEY -> KeyOperation.SYMMETRIC_IMPORT; + }; + return algorithm.keyOperations().stream() + .filter(info -> info.operation() == expectedOperation) + .map(KeyOperationInfo::specType) + .filter(type -> type.getName().equals(persistedClassName)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + "Specification class is not registered for " + algorithmId + " " + kind)); + } + + /* default */ static void destroyTemporarySpec(AlgorithmKeySpec spec, Throwable primary) + throws GeneralSecurityException { + if (!(spec instanceof Destroyable destroyable)) { + return; + } + try { + if (!destroyable.isDestroyed()) { + destroyable.destroy(); + } + } catch (DestroyFailedException failure) { + if (primary != null) { + primary.addSuppressed(failure); + return; + } + throw new GeneralSecurityException("Temporary key specification destruction failed", failure); + } catch (RuntimeException failure) { // NOPMD - preserve cleanup failure and primary failure + if (primary != null) { + primary.addSuppressed(failure); + return; + } + throw failure; } } } diff --git a/lib/src/main/java/zeroecho/core/storage/package-info.java b/lib/src/main/java/zeroecho/core/storage/package-info.java index ce7a43a..8797532 100644 --- a/lib/src/main/java/zeroecho/core/storage/package-info.java +++ b/lib/src/main/java/zeroecho/core/storage/package-info.java @@ -85,20 +85,21 @@ *
    139. static SpecType unmarshal(PairSeq pairs)
    140. * *

      - * See {@link KeyringStore#marshalSpec(zeroecho.core.spec.AlgorithmKeySpec)} and - * {@link KeyringStore#unmarshalSpec(String, zeroecho.core.marshal.PairSeq)} for - * details. + * {@link KeyringStore} validates each persisted specification type against the + * selected algorithm's registered import operation before invoking these + * methods. *

      * *

      Typical usage

      {@code
        * // Create and persist a keyring.
      - * KeyringStore ks = new KeyringStore();
      + * zeroecho.sdk.ZeroEchoSession session = new zeroecho.sdk.ZeroEchoSession();
      + * KeyringStore ks = new KeyringStore(session);
        * ks.putPublic("site-signing", "Ed25519", myEd25519PublicSpec);
        * ks.putPrivate("site-signing", "Ed25519", myEd25519PrivateSpec);
        * ks.save(java.nio.file.Path.of("keyring.txt"));
        *
        * // Load and resolve a key later.
      - * KeyringStore reloaded = KeyringStore.load(java.nio.file.Path.of("keyring.txt"));
      + * KeyringStore reloaded = KeyringStore.load(session, java.nio.file.Path.of("keyring.txt"));
        * java.security.PublicKey pub = reloaded.getPublic("site-signing");
        * }
      * diff --git a/lib/src/main/java/zeroecho/core/tag/TagEngineBuilder.java b/lib/src/main/java/zeroecho/core/tag/TagEngineBuilder.java index 2e3b57c..2aeba64 100644 --- a/lib/src/main/java/zeroecho/core/tag/TagEngineBuilder.java +++ b/lib/src/main/java/zeroecho/core/tag/TagEngineBuilder.java @@ -33,7 +33,6 @@ ******************************************************************************/ package zeroecho.core.tag; -import java.io.IOException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; @@ -42,7 +41,6 @@ import java.util.function.Supplier; import javax.crypto.SecretKey; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.KeyUsage; import zeroecho.core.NullKey; import zeroecho.core.alg.digest.DigestSpec; @@ -50,6 +48,7 @@ import zeroecho.core.alg.ecdsa.EcdsaCurveSpec; import zeroecho.core.alg.hmac.HmacSpec; import zeroecho.core.alg.rsa.RsaSigSpec; import zeroecho.core.spec.ContextSpec; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.core.spec.VoidSpec; /** @@ -58,8 +57,8 @@ import zeroecho.core.spec.VoidSpec; * *

      * Each {@code TagEngineBuilder} holds a factory that typically delegates to - * {@link CryptoAlgorithms#create(String, KeyUsage, java.security.Key, ContextSpec)} - * so that global policy and auditing are consistently enforced. A new engine + * {@link ZeroEchoSession#createContext(String, KeyUsage, java.security.Key, ContextSpec)} + * so that session policy and auditing are consistently enforced. A new engine * instance is created for each call to {@link #get()}. *

      * @@ -122,16 +121,11 @@ public final class TagEngineBuilder implements Supplier> { * @param spec digest specification; may be {@code null} to select the default * @return a builder that produces digest-based {@link TagEngine} instances */ - public static TagEngineBuilder digest(final DigestSpec spec) { + public static TagEngineBuilder digest(final ZeroEchoSession session, final DigestSpec spec) { + Objects.requireNonNull(session, "session"); final DigestSpec s = spec == null ? DigestSpec.sha256() : spec; - return new TagEngineBuilder<>(() -> { - try { - // JcaDigestContext implements DigestContext extends TagEngine - return CryptoAlgorithms.create("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, s); - } catch (IOException e) { - throw new IllegalStateException("Failed to create DIGEST TagEngine", e); - } - }); + return new TagEngineBuilder<>( + () -> session.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, s)); } /** @@ -146,17 +140,12 @@ public final class TagEngineBuilder implements Supplier> { * @return a builder that produces HMAC-based {@link TagEngine} instances * @throws NullPointerException if {@code key} is {@code null} */ - public static TagEngineBuilder hmac(final SecretKey key, final HmacSpec spec) { + public static TagEngineBuilder hmac(final ZeroEchoSession session, final SecretKey key, + final HmacSpec spec) { + Objects.requireNonNull(session, "session"); Objects.requireNonNull(key, "key"); final HmacSpec s = spec == null ? HmacSpec.sha256() : spec; - return new TagEngineBuilder<>(() -> { - try { - // HmacMacContext implements MacContext extends TagEngine - return CryptoAlgorithms.create("HMAC", KeyUsage.MAC, key, s); - } catch (IOException e) { - throw new IllegalStateException("Failed to create HMAC TagEngine", e); - } - }); + return new TagEngineBuilder<>(() -> session.createContext("HMAC", KeyUsage.MAC, key, s)); } /** @@ -181,8 +170,9 @@ public final class TagEngineBuilder implements Supplier> { * @throws IllegalArgumentException if {@code key} is not a supported type * @throws NullPointerException if {@code id} or {@code key} is {@code null} */ - public static TagEngineBuilder signature(final String id, final java.security.Key key, - final ContextSpec spec) { + public static TagEngineBuilder signature(final ZeroEchoSession session, final String id, + final java.security.Key key, final ContextSpec spec) { + Objects.requireNonNull(session, "session"); Objects.requireNonNull(id, "id"); Objects.requireNonNull(key, "key"); @@ -193,15 +183,7 @@ public final class TagEngineBuilder implements Supplier> { } final ContextSpec s = spec == null ? VoidSpec.INSTANCE : spec; - return new TagEngineBuilder<>(() -> { - try { - // RsaSignatureContext / Ed25519SignatureContext implement SignatureContext - // extends TagEngine - return CryptoAlgorithms.create(id, role, key, s); - } catch (IOException e) { - throw new IllegalStateException("Failed to create " + id + " signature TagEngine", e); - } - }); + return new TagEngineBuilder<>(() -> session.createContext(id, role, key, s)); } /** @@ -211,9 +193,10 @@ public final class TagEngineBuilder implements Supplier> { * @return a builder that produces Ed25519 signature engines in SIGN mode * @throws NullPointerException if {@code privateKey} is {@code null} */ - public static TagEngineBuilder ed25519Sign(final PrivateKey privateKey) { + public static TagEngineBuilder ed25519Sign(final ZeroEchoSession session, + final PrivateKey privateKey) { Objects.requireNonNull(privateKey, PRIVATE_KEY); - return signature("Ed25519", privateKey, VoidSpec.INSTANCE); + return signature(session, "Ed25519", privateKey, VoidSpec.INSTANCE); } /** @@ -223,9 +206,10 @@ public final class TagEngineBuilder implements Supplier> { * @return a builder that produces Ed25519 signature engines in VERIFY mode * @throws NullPointerException if {@code publicKey} is {@code null} */ - public static TagEngineBuilder ed25519Verify(final PublicKey publicKey) { + public static TagEngineBuilder ed25519Verify(final ZeroEchoSession session, + final PublicKey publicKey) { Objects.requireNonNull(publicKey, PUBLIC_KEY); - return signature("Ed25519", publicKey, VoidSpec.INSTANCE); + return signature(session, "Ed25519", publicKey, VoidSpec.INSTANCE); } /** @@ -242,9 +226,11 @@ public final class TagEngineBuilder implements Supplier> { * @return a builder that produces RSA signature engines in SIGN mode * @throws NullPointerException if {@code privateKey} is {@code null} */ - public static TagEngineBuilder rsaSign(final PrivateKey privateKey, final RsaSigSpec spec) { + public static TagEngineBuilder rsaSign(final ZeroEchoSession session, final PrivateKey privateKey, + final RsaSigSpec spec) { Objects.requireNonNull(privateKey, PRIVATE_KEY); - return signature("RSA", privateKey, spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec); + return signature(session, "RSA", privateKey, + spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec); } /** @@ -261,9 +247,11 @@ public final class TagEngineBuilder implements Supplier> { * @return a builder that produces RSA signature engines in VERIFY mode * @throws NullPointerException if {@code publicKey} is {@code null} */ - public static TagEngineBuilder rsaVerify(final PublicKey publicKey, final RsaSigSpec spec) { + public static TagEngineBuilder rsaVerify(final ZeroEchoSession session, final PublicKey publicKey, + final RsaSigSpec spec) { Objects.requireNonNull(publicKey, PUBLIC_KEY); - return signature("RSA", publicKey, spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec); + return signature(session, "RSA", publicKey, + spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec); } /** @@ -279,10 +267,11 @@ public final class TagEngineBuilder implements Supplier> { * @return a builder that produces ECDSA signature engines in SIGN mode * @throws NullPointerException if {@code privateKey} is {@code null} */ - public static TagEngineBuilder ecdsaSign(final PrivateKey privateKey, final EcdsaCurveSpec spec) { + public static TagEngineBuilder ecdsaSign(final ZeroEchoSession session, final PrivateKey privateKey, + final EcdsaCurveSpec spec) { Objects.requireNonNull(privateKey, PRIVATE_KEY); final EcdsaCurveSpec s = spec == null ? EcdsaCurveSpec.P256 : spec; - return signature("ECDSA", privateKey, s); + return signature(session, "ECDSA", privateKey, s); } /** @@ -298,10 +287,11 @@ public final class TagEngineBuilder implements Supplier> { * @return a builder that produces ECDSA signature engines in VERIFY mode * @throws NullPointerException if {@code publicKey} is {@code null} */ - public static TagEngineBuilder ecdsaVerify(final PublicKey publicKey, final EcdsaCurveSpec spec) { + public static TagEngineBuilder ecdsaVerify(final ZeroEchoSession session, final PublicKey publicKey, + final EcdsaCurveSpec spec) { Objects.requireNonNull(publicKey, PUBLIC_KEY); final EcdsaCurveSpec s = spec == null ? EcdsaCurveSpec.P256 : spec; - return signature("ECDSA", publicKey, s); + return signature(session, "ECDSA", publicKey, s); } /** @@ -311,9 +301,10 @@ public final class TagEngineBuilder implements Supplier> { * @return a builder that produces ECDSA signature engines in SIGN mode * @throws NullPointerException if {@code privateKey} is {@code null} */ - public static TagEngineBuilder ecdsaP256Sign(final PrivateKey privateKey) { + public static TagEngineBuilder ecdsaP256Sign(final ZeroEchoSession session, + final PrivateKey privateKey) { Objects.requireNonNull(privateKey, PRIVATE_KEY); - return signature("ECDSA", privateKey, EcdsaCurveSpec.P256); + return signature(session, "ECDSA", privateKey, EcdsaCurveSpec.P256); } /** @@ -323,9 +314,10 @@ public final class TagEngineBuilder implements Supplier> { * @return a builder that produces ECDSA signature engines in VERIFY mode * @throws NullPointerException if {@code publicKey} is {@code null} */ - public static TagEngineBuilder ecdsaP256Verify(final PublicKey publicKey) { + public static TagEngineBuilder ecdsaP256Verify(final ZeroEchoSession session, + final PublicKey publicKey) { Objects.requireNonNull(publicKey, PUBLIC_KEY); - return signature("ECDSA", publicKey, EcdsaCurveSpec.P256); + return signature(session, "ECDSA", publicKey, EcdsaCurveSpec.P256); } /** @@ -333,16 +325,17 @@ public final class TagEngineBuilder implements Supplier> { * *

      * Requires the BouncyCastle PQC provider and a registered "SPHINCS+" algorithm - * in {@link CryptoAlgorithms}. + * in the supplied {@link ZeroEchoSession}. *

      * * @param privateKey private signing key; must not be {@code null} * @return a builder that produces SPHINCS+ signature engines in SIGN mode * @throws NullPointerException if {@code privateKey} is {@code null} */ - public static TagEngineBuilder sphincsPlusSign(final PrivateKey privateKey) { + public static TagEngineBuilder sphincsPlusSign(final ZeroEchoSession session, + final PrivateKey privateKey) { Objects.requireNonNull(privateKey, PRIVATE_KEY); - return signature("SPHINCS+", privateKey, VoidSpec.INSTANCE); + return signature(session, "SPHINCS+", privateKey, VoidSpec.INSTANCE); } /** @@ -350,16 +343,17 @@ public final class TagEngineBuilder implements Supplier> { * *

      * Requires the BouncyCastle PQC provider and a registered "SPHINCS+" algorithm - * in {@link CryptoAlgorithms}. + * in the supplied {@link ZeroEchoSession}. *

      * * @param publicKey public verification key; must not be {@code null} * @return a builder that produces SPHINCS+ signature engines in VERIFY mode * @throws NullPointerException if {@code publicKey} is {@code null} */ - public static TagEngineBuilder sphincsPlusVerify(final PublicKey publicKey) { + public static TagEngineBuilder sphincsPlusVerify(final ZeroEchoSession session, + final PublicKey publicKey) { Objects.requireNonNull(publicKey, PUBLIC_KEY); - return signature("SPHINCS+", publicKey, VoidSpec.INSTANCE); + return signature(session, "SPHINCS+", publicKey, VoidSpec.INSTANCE); } /** @@ -368,16 +362,17 @@ public final class TagEngineBuilder implements Supplier> { *

      * SLH-DSA is the NIST-standardized hash-based signature scheme (FIPS 205). The * concrete parameter set is encoded in the key material and interpreted by the - * underlying {@link CryptoAlgorithms} implementation. + * algorithm resolved through the supplied {@link ZeroEchoSession}. *

      * * @param privateKey private signing key; must not be {@code null} * @return a builder that produces SLH-DSA signature engines in SIGN mode * @throws NullPointerException if {@code privateKey} is {@code null} */ - public static TagEngineBuilder slhDsaSign(final PrivateKey privateKey) { + public static TagEngineBuilder slhDsaSign(final ZeroEchoSession session, + final PrivateKey privateKey) { Objects.requireNonNull(privateKey, PRIVATE_KEY); - return signature("SLH-DSA", privateKey, VoidSpec.INSTANCE); + return signature(session, "SLH-DSA", privateKey, VoidSpec.INSTANCE); } /** @@ -386,16 +381,17 @@ public final class TagEngineBuilder implements Supplier> { *

      * SLH-DSA is the NIST-standardized hash-based signature scheme (FIPS 205). The * concrete parameter set is encoded in the key material and interpreted by the - * underlying {@link CryptoAlgorithms} implementation. + * algorithm resolved through the supplied {@link ZeroEchoSession}. *

      * * @param publicKey public verification key; must not be {@code null} * @return a builder that produces SLH-DSA signature engines in VERIFY mode * @throws NullPointerException if {@code publicKey} is {@code null} */ - public static TagEngineBuilder slhDsaVerify(final PublicKey publicKey) { + public static TagEngineBuilder slhDsaVerify(final ZeroEchoSession session, + final PublicKey publicKey) { Objects.requireNonNull(publicKey, PUBLIC_KEY); - return signature("SLH-DSA", publicKey, VoidSpec.INSTANCE); + return signature(session, "SLH-DSA", publicKey, VoidSpec.INSTANCE); } /** @@ -404,17 +400,18 @@ public final class TagEngineBuilder implements Supplier> { *

      * ML-DSA is the NIST-standardized module-lattice signature scheme (FIPS 204). * The concrete parameter set and any pre-hash variant is encoded in the key - * material and interpreted by the underlying {@link CryptoAlgorithms} - * implementation. + * material and interpreted by the algorithm resolved through the supplied + * {@link ZeroEchoSession}. *

      * * @param privateKey private signing key; must not be {@code null} * @return a builder that produces ML-DSA signature engines in SIGN mode * @throws NullPointerException if {@code privateKey} is {@code null} */ - public static TagEngineBuilder mldsaSign(final PrivateKey privateKey) { + public static TagEngineBuilder mldsaSign(final ZeroEchoSession session, + final PrivateKey privateKey) { Objects.requireNonNull(privateKey, PRIVATE_KEY); - return signature("ML-DSA", privateKey, VoidSpec.INSTANCE); + return signature(session, "ML-DSA", privateKey, VoidSpec.INSTANCE); } /** @@ -423,16 +420,17 @@ public final class TagEngineBuilder implements Supplier> { *

      * ML-DSA is the NIST-standardized module-lattice signature scheme (FIPS 204). * The concrete parameter set and any pre-hash variant is encoded in the key - * material and interpreted by the underlying {@link CryptoAlgorithms} - * implementation. + * material and interpreted by the algorithm resolved through the supplied + * {@link ZeroEchoSession}. *

      * * @param publicKey public verification key; must not be {@code null} * @return a builder that produces ML-DSA signature engines in VERIFY mode * @throws NullPointerException if {@code publicKey} is {@code null} */ - public static TagEngineBuilder mldsaVerify(final PublicKey publicKey) { + public static TagEngineBuilder mldsaVerify(final ZeroEchoSession session, + final PublicKey publicKey) { Objects.requireNonNull(publicKey, PUBLIC_KEY); - return signature("ML-DSA", publicKey, VoidSpec.INSTANCE); + return signature(session, "ML-DSA", publicKey, VoidSpec.INSTANCE); } } diff --git a/lib/src/main/java/zeroecho/core/util/GenerateCryptoCatalogTable.java b/lib/src/main/java/zeroecho/core/util/GenerateCryptoCatalogTable.java index 2d678e6..435d203 100644 --- a/lib/src/main/java/zeroecho/core/util/GenerateCryptoCatalogTable.java +++ b/lib/src/main/java/zeroecho/core/util/GenerateCryptoCatalogTable.java @@ -37,17 +37,15 @@ import java.io.File; import java.io.Writer; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; -import java.util.Map; -import java.util.ServiceLoader; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import zeroecho.core.Capability; import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; import zeroecho.sdk.util.BouncyCastleActivator; /** @@ -92,14 +90,14 @@ public final class GenerateCryptoCatalogTable { File out = new File(args[0]); out.getParentFile().mkdirs(); - // Discover providers directly so we do not rely on CryptoCatalog internals. - Map algos = loadAlgorithms(); - validate(algos); + Set algorithmIds = CryptoAlgorithms.available(); + validate(algorithmIds); // Enumerate subcolumns actually present. SortedSet families = new TreeSet<>(); SortedSet roles = new TreeSet<>(); - for (CryptoAlgorithm a : algos.values()) { + for (String algorithmId : algorithmIds) { + CryptoAlgorithm a = CryptoAlgorithms.require(algorithmId); for (Capability c : a.listCapabilities()) { families.add(c.family().name()); roles.add(c.role().name()); @@ -144,8 +142,8 @@ public final class GenerateCryptoCatalogTable { // TBODY same as before, but make family/role cells narrow and centered w.write(""); - for (String id : new TreeSet<>(algos.keySet())) { - CryptoAlgorithm a = algos.get(id); + for (String id : algorithmIds) { + CryptoAlgorithm a = CryptoAlgorithms.require(id); Set famHit = new HashSet<>(); // NOPMD Set roleHit = new HashSet<>(); // NOPMD @@ -156,7 +154,7 @@ public final class GenerateCryptoCatalogTable { roleHit.add(c.role().name()); if (c.defaultSpec() != null) { try { - Object ds = c.defaultSpec().get(); + Object ds = c.defaultSpec(); if (ds != null) { defaultSpecs.add(esc(labelOf(ds))); } @@ -189,24 +187,13 @@ public final class GenerateCryptoCatalogTable { } } - private static Map loadAlgorithms() { - Map m = new HashMap<>(); - ServiceLoader.load(CryptoAlgorithm.class).forEach(a -> { - CryptoAlgorithm prev = m.put(a.id(), a); - if (prev != null) { - throw new IllegalStateException("Duplicate algorithm id: " + a.id()); - } - }); - return m; - } - - private static void validate(Map algos) { + private static void validate(Set algorithmIds) { StringBuilder sb = null; - for (CryptoAlgorithm a : algos.values()) { + for (String algorithmId : algorithmIds) { + CryptoAlgorithm a = CryptoAlgorithms.require(algorithmId); boolean hasCaps = !a.listCapabilities().isEmpty(); - boolean hasAsym = !a.asymmetricBuildersInfo().isEmpty(); - boolean hasSym = !a.symmetricBuildersInfo().isEmpty(); - if (!hasCaps && !hasAsym && !hasSym) { + boolean hasKeyOperations = !a.keyOperations().isEmpty(); + if (!hasCaps && !hasKeyOperations) { if (sb == null) { sb = new StringBuilder(); // NOPMD } diff --git a/lib/src/main/java/zeroecho/sdk/KeyBuilders.java b/lib/src/main/java/zeroecho/sdk/KeyBuilders.java new file mode 100644 index 0000000..978d91e --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/KeyBuilders.java @@ -0,0 +1,271 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the conditions in the project LICENSE are met. + ******************************************************************************/ +package zeroecho.sdk; + +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Objects; + +import javax.crypto.SecretKey; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; +import zeroecho.core.spi.SymmetricKeyGenerator; +import zeroecho.core.spi.SymmetricKeyImporter; + +/** + * Session-bound entry point for exact key-material operations. + * + *

      Capability lookup fails before an operation object is returned. Returned + * objects guarantee the requested operation and report successful execution to + * the owning session's audit listener on a best-effort basis.

      + * + * @since 1.0 + */ +public final class KeyBuilders { + private final ZeroEchoSession session; + private final Symmetric symmetric = new Symmetric(); + private final Asymmetric asymmetric = new Asymmetric(); + + /* default */ KeyBuilders(ZeroEchoSession session) { + this.session = Objects.requireNonNull(session, "session must not be null"); + } + + /** + * Returns symmetric key operations. + * + * @return session-bound symmetric namespace + */ + public Symmetric symmetric() { + return symmetric; + } + + /** + * Returns asymmetric key operations. + * + * @return session-bound asymmetric namespace + */ + public Asymmetric asymmetric() { + return asymmetric; + } + + /** + * Symmetric generation and import lookups. + */ + public final class Symmetric { + private Symmetric() { + } + + /** + * Resolves an exact symmetric generator. + * + * @param algorithmId canonical algorithm identifier + * @param specType exact specification class + * @param specification type + * @return guaranteed generator + * @throws IllegalArgumentException if the capability is absent + */ + public SymmetricKeyGenerator generator(String algorithmId, + Class specType) { + CryptoAlgorithm algorithm = session.require(algorithmId); + SymmetricKeyGenerator delegate = algorithm.symmetricKeyGenerator(specType); + return spec -> { + SecretKey key = delegate.generateSecret(spec); + session.notifyKeyGenerated(algorithm, spec, key); + return key; + }; + } + + /** + * Resolves an exact symmetric importer. + * + * @param algorithmId canonical algorithm identifier + * @param specType exact specification class + * @param specification type + * @return guaranteed importer + * @throws IllegalArgumentException if the capability is absent + */ + public SymmetricKeyImporter importer(String algorithmId, + Class specType) { + CryptoAlgorithm algorithm = session.require(algorithmId); + SymmetricKeyImporter delegate = algorithm.symmetricKeyImporter(specType); + return spec -> { + SecretKey key = delegate.importSecret(spec); + session.notifyKeyBuilt(algorithm, spec, key); + return key; + }; + } + + /** + * Generates a symmetric key using the exact runtime specification type. + * + * @param algorithmId canonical algorithm identifier + * @param spec generation specification + * @param specification type + * @return generated secret key + * @throws java.security.GeneralSecurityException if generation fails + * @throws IllegalArgumentException if the capability is absent + * @throws NullPointerException if {@code spec} is {@code null} + */ + public SecretKey generate(String algorithmId, S spec) + throws java.security.GeneralSecurityException { + Objects.requireNonNull(spec, "spec"); + @SuppressWarnings("unchecked") + Class specType = (Class) spec.getClass(); + return generator(algorithmId, specType).generateSecret(spec); + } + + /** + * Imports a symmetric key using the exact runtime specification type. + * + * @param algorithmId canonical algorithm identifier + * @param spec import specification + * @param specification type + * @return imported secret key + * @throws java.security.GeneralSecurityException if import fails + * @throws IllegalArgumentException if the capability is absent + * @throws NullPointerException if {@code spec} is {@code null} + */ + public SecretKey importKey(String algorithmId, S spec) + throws java.security.GeneralSecurityException { + Objects.requireNonNull(spec, "spec"); + @SuppressWarnings("unchecked") + Class specType = (Class) spec.getClass(); + return importer(algorithmId, specType).importSecret(spec); + } + } + + /** + * Asymmetric generation and import lookups. + */ + public final class Asymmetric { + private Asymmetric() { + } + + /** + * Resolves an exact key-pair generator. + * + * @param algorithmId canonical algorithm identifier + * @param specType exact specification class + * @param specification type + * @return guaranteed generator + * @throws IllegalArgumentException if the capability is absent + */ + public AsymmetricKeyPairGenerator keyPairGenerator(String algorithmId, + Class specType) { + CryptoAlgorithm algorithm = session.require(algorithmId); + AsymmetricKeyPairGenerator delegate = algorithm.asymmetricKeyPairGenerator(specType); + return spec -> { + KeyPair pair = delegate.generateKeyPair(spec); + session.notifyKeyPairGenerated(algorithm, spec, pair); + return pair; + }; + } + + /** + * Resolves an exact public-key importer. + * + * @param algorithmId canonical algorithm identifier + * @param specType exact specification class + * @param specification type + * @return guaranteed importer + * @throws IllegalArgumentException if the capability is absent + */ + public PublicKeyImporter publicImporter(String algorithmId, + Class specType) { + CryptoAlgorithm algorithm = session.require(algorithmId); + PublicKeyImporter delegate = algorithm.publicKeyImporter(specType); + return spec -> { + PublicKey key = delegate.importPublic(spec); + session.notifyKeyBuilt(algorithm, spec, key); + return key; + }; + } + + /** + * Resolves an exact private-key importer. + * + * @param algorithmId canonical algorithm identifier + * @param specType exact specification class + * @param specification type + * @return guaranteed importer + * @throws IllegalArgumentException if the capability is absent + */ + public PrivateKeyImporter privateImporter(String algorithmId, + Class specType) { + CryptoAlgorithm algorithm = session.require(algorithmId); + PrivateKeyImporter delegate = algorithm.privateKeyImporter(specType); + return spec -> { + PrivateKey key = delegate.importPrivate(spec); + session.notifyKeyBuilt(algorithm, spec, key); + return key; + }; + } + + /** + * Generates a key pair using the exact runtime specification type. + * + * @param algorithmId canonical algorithm identifier + * @param spec generation specification + * @param specification type + * @return generated key pair + * @throws java.security.GeneralSecurityException if generation fails + * @throws IllegalArgumentException if the capability is absent + * @throws NullPointerException if {@code spec} is {@code null} + */ + public KeyPair generateKeyPair(String algorithmId, S spec) + throws java.security.GeneralSecurityException { + Objects.requireNonNull(spec, "spec"); + @SuppressWarnings("unchecked") + Class specType = (Class) spec.getClass(); + return keyPairGenerator(algorithmId, specType).generateKeyPair(spec); + } + + /** + * Imports a public key using the exact runtime specification type. + * + * @param algorithmId canonical algorithm identifier + * @param spec public-key import specification + * @param specification type + * @return imported public key + * @throws java.security.GeneralSecurityException if import fails + * @throws IllegalArgumentException if the capability is absent + * @throws NullPointerException if {@code spec} is {@code null} + */ + public PublicKey importPublic(String algorithmId, S spec) + throws java.security.GeneralSecurityException { + Objects.requireNonNull(spec, "spec"); + @SuppressWarnings("unchecked") + Class specType = (Class) spec.getClass(); + return publicImporter(algorithmId, specType).importPublic(spec); + } + + /** + * Imports a private key using the exact runtime specification type. + * + * @param algorithmId canonical algorithm identifier + * @param spec private-key import specification + * @param specification type + * @return imported private key + * @throws java.security.GeneralSecurityException if import fails + * @throws IllegalArgumentException if the capability is absent + * @throws NullPointerException if {@code spec} is {@code null} + */ + public PrivateKey importPrivate(String algorithmId, S spec) + throws java.security.GeneralSecurityException { + Objects.requireNonNull(spec, "spec"); + @SuppressWarnings("unchecked") + Class specType = (Class) spec.getClass(); + return privateImporter(algorithmId, specType).importPrivate(spec); + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/Pbkdf2Limits.java b/lib/src/main/java/zeroecho/sdk/Pbkdf2Limits.java new file mode 100644 index 0000000..3cb708d --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/Pbkdf2Limits.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the conditions in the project LICENSE are met. + ******************************************************************************/ +package zeroecho.sdk; + +/** + * Explicit PBKDF2 work-factor limits for trusted configuration and decoded data. + * + * @param operationalMaximum largest iteration count accepted from trusted local + * configuration + * @param absoluteDecodedMaximum hard safety ceiling for untrusted decoded data + * @since 1.0 + */ +public record Pbkdf2Limits(int operationalMaximum, int absoluteDecodedMaximum) { + /** Mandatory minimum PBKDF2 iteration count. */ + public static final int MINIMUM = 10_000; + + /** + * Validates {@code minimum <= operationalMaximum <= absoluteDecodedMaximum}. + * + * @throws IllegalArgumentException if the limits violate the ordering + */ + public Pbkdf2Limits { + if (operationalMaximum < MINIMUM) { + throw new IllegalArgumentException("operationalMaximum must be at least " + MINIMUM); + } + if (absoluteDecodedMaximum < operationalMaximum) { + throw new IllegalArgumentException("absoluteDecodedMaximum must be at least operationalMaximum"); + } + } + + /** + * Validates trusted local configuration. + * + * @param iterations requested iteration count + * @throws IllegalArgumentException if outside the operational range + */ + public void validateTrusted(int iterations) { + if (iterations < MINIMUM || iterations > operationalMaximum) { + throw new IllegalArgumentException("PBKDF2 iterations must be in range " + MINIMUM + ".." + + operationalMaximum + ": " + iterations); + } + } + + /** + * Validates an untrusted decoded iteration count before KDF execution. + * + * @param iterations decoded iteration count + * @throws IllegalArgumentException if outside the absolute safety range + */ + public void validateDecoded(int iterations) { + if (iterations < MINIMUM || iterations > absoluteDecodedMaximum) { + throw new IllegalArgumentException("Decoded PBKDF2 iterations must be in range " + MINIMUM + ".." + + absoluteDecodedMaximum + ": " + iterations); + } + if (iterations > operationalMaximum) { + throw new IllegalArgumentException("Decoded PBKDF2 iterations exceed the session policy maximum " + + operationalMaximum + ": " + iterations); + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/ZeroEchoSession.java b/lib/src/main/java/zeroecho/sdk/ZeroEchoSession.java new file mode 100644 index 0000000..b0beb7b --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/ZeroEchoSession.java @@ -0,0 +1,382 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk; + +import java.security.Key; +import java.security.KeyPair; +import java.util.Objects; +import java.util.Set; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.DestroyFailedException; +import javax.security.auth.Destroyable; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.audit.AuditListener; +import zeroecho.core.audit.AuditMode; +import zeroecho.core.audit.AuditedContexts; +import zeroecho.core.audit.AuditListeners; +import zeroecho.core.context.AgreementContext; +import zeroecho.core.context.CryptoContext; +import zeroecho.core.context.DigestContext; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.MacContext; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.err.UnsupportedRoleException; +import zeroecho.core.err.UnsupportedSpecException; +import zeroecho.core.policy.CryptoPolicy; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spec.ContextSpec; + +/** + * Immutable, explicitly scoped runtime configuration for ZeroEcho operations. + * + *

      + * Each session owns one policy, audit listener, and audit mode. Configuration + * methods return a new session and never mutate the receiver, so independently + * created sessions cannot affect one another. The default configuration is a + * permissive policy, a no-op listener, and {@link AuditMode#OFF}. + *

      + * + *

      + * Sessions share the immutable provider registry owned by + * {@link CryptoAlgorithms}; provider discovery is therefore performed once for + * the registry lifecycle. Session instances are safe for concurrent use. + * Configured policy and listener implementations must themselves support any + * concurrency with which the session is used. + *

      + * + *

      + * Audit callbacks may receive key and specification objects. Listeners are + * trusted application components and must not log or retain keys, plaintext, + * seeds, shared secrets, or sensitive specifications. + *

      + * + * @since 1.0 + */ +public final class ZeroEchoSession { + private static final int DESTROY_LOCK_STRIPES = 64; + private static final ReentrantLock[] DESTROY_LOCKS = createDestroyLocks(); + private final CryptoPolicy policy; + private final AuditListener auditListener; + private final AuditListener auditSink; + private final AuditMode auditMode; + private final KeyBuilders keyBuilders; + private final Pbkdf2Limits pbkdf2Limits; + + /** + * Creates a session with a permissive policy, no-op audit listener, and + * {@link AuditMode#OFF}. + */ + public ZeroEchoSession() { + this(CryptoPolicy.permissive(), AuditListener.noop(), AuditMode.OFF, null); + } + + private ZeroEchoSession(CryptoPolicy policy, AuditListener auditListener, + AuditMode auditMode, Pbkdf2Limits pbkdf2Limits) { + this.policy = Objects.requireNonNull(policy, "policy must not be null"); + this.auditListener = Objects.requireNonNull(auditListener, "auditListener must not be null"); + this.auditSink = AuditListeners.bestEffort(auditListener); + this.auditMode = Objects.requireNonNull(auditMode, "auditMode must not be null"); + this.pbkdf2Limits = pbkdf2Limits; + this.keyBuilders = new KeyBuilders(this); + } + + /** + * Returns a session with the supplied policy and this session's audit + * configuration. + * + * @param newPolicy policy applied before context creation; must not be + * {@code null} + * @return a new independently configured session + * @throws NullPointerException if {@code newPolicy} is {@code null} + */ + public ZeroEchoSession withPolicy(CryptoPolicy newPolicy) { + return new ZeroEchoSession(Objects.requireNonNull(newPolicy, "newPolicy must not be null"), auditListener, + auditMode, pbkdf2Limits); + } + + /** + * Returns a session with the supplied audit listener and this session's policy + * and audit mode. + * + * @param newAuditListener listener receiving audit callbacks; must not be + * {@code null} + * @return a new independently configured session + * @throws NullPointerException if {@code newAuditListener} is {@code null} + */ + public ZeroEchoSession withAuditListener(AuditListener newAuditListener) { + return new ZeroEchoSession(policy, + Objects.requireNonNull(newAuditListener, "newAuditListener must not be null"), auditMode, + pbkdf2Limits); + } + + /** + * Returns a session with the supplied audit mode and this session's policy and + * listener. + * + * @param newAuditMode audit strategy; must not be {@code null} + * @return a new independently configured session + * @throws NullPointerException if {@code newAuditMode} is {@code null} + */ + public ZeroEchoSession withAuditMode(AuditMode newAuditMode) { + return new ZeroEchoSession(policy, auditListener, + Objects.requireNonNull(newAuditMode, "newAuditMode must not be null"), pbkdf2Limits); + } + + /** + * Returns a session with explicit PBKDF2 work-factor limits. + * + * @param limits deployment-selected limits + * @return independently configured session + * @throws NullPointerException if {@code limits} is {@code null} + */ + public ZeroEchoSession withPbkdf2Limits(Pbkdf2Limits limits) { + return new ZeroEchoSession(policy, auditListener, auditMode, + Objects.requireNonNull(limits, "limits must not be null")); + } + + /** + * Returns configured PBKDF2 limits. + * + * @return explicit deployment limits + * @throws IllegalStateException if limits were not configured + */ + public Pbkdf2Limits pbkdf2Limits() { + if (pbkdf2Limits == null) { + throw new IllegalStateException("PBKDF2 limits must be configured explicitly"); + } + return pbkdf2Limits; + } + + /** + * Returns the policy owned by this session. + * + * @return the non-null policy strategy + */ + public CryptoPolicy policy() { + return policy; + } + + /** + * Returns the audit listener owned by this session. + * + *

      + * The returned listener is the configured strategy, not mutable session + * state. It is exposed to support manual audit mode. + *

      + * + * @return the non-null audit listener + */ + public AuditListener auditListener() { + return auditListener; + } + + /** + * Returns the audit mode owned by this session. + * + * @return the non-null audit mode + */ + public AuditMode auditMode() { + return auditMode; + } + + /** + * Returns the available algorithm identifiers in deterministic registry order. + * + * @return an unmodifiable set of canonical algorithm identifiers + */ + public Set available() { + return CryptoAlgorithms.available(); + } + + /** + * Resolves an algorithm from the authoritative registry. + * + * @param id canonical algorithm identifier + * @return the registered algorithm + * @throws IllegalArgumentException if no algorithm is registered under + * {@code id} + */ + public CryptoAlgorithm require(String id) { + return CryptoAlgorithms.require(id); + } + + /** + * Returns exact session-bound key operations. + * + * @return immutable grouped key-operation entry point + */ + public KeyBuilders keyBuilders() { + return keyBuilders; + } + + /** + * Creates a context after applying this session's policy and audit + * configuration. + * + * @param id canonical algorithm identifier + * @param role intended key usage + * @param key key compatible with the selected algorithm and role + * @param spec optional context specification, or {@code null} for the + * algorithm default + * @param context type + * @param key type + * @param context specification type + * @return a ready context, possibly audit-wrapped in {@link AuditMode#WRAP} + * @throws IllegalArgumentException if the algorithm identifier is unknown or + * policy validation rejects the operation + * @throws UnsupportedRoleException if the algorithm does not support + * {@code role} + * @throws UnsupportedSpecException if the key or specification is incompatible + */ + public C createContext(String id, KeyUsage role, + K key, S spec) { + policy.validate(id, role, key, spec); + + CryptoAlgorithm algorithm = require(id); + C context = algorithm.createContext(role, key, spec); + return finishContext(algorithm, context, role, spec); + } + + private C finishContext(CryptoAlgorithm algorithm, + C context, KeyUsage role, S spec) { + if (auditMode == AuditMode.OFF) { + notifyContextCreated(algorithm, role, spec); + return context; + } + if (auditMode == AuditMode.WRAP) { + return wrapForAudit(context, role); + } + return context; + } + + /** + * Creates a context using the selected algorithm's default specification. + * + * @param id canonical algorithm identifier + * @param role intended key usage + * @param key key compatible with the selected algorithm and role + * @param context type + * @param key type + * @return a ready context, possibly audit-wrapped in {@link AuditMode#WRAP} + * @throws IllegalArgumentException if the algorithm identifier is unknown or + * policy validation rejects the operation + * @throws UnsupportedRoleException if the algorithm does not support + * {@code role} + */ + public C createContext(String id, KeyUsage role, K key) { + return createContext(id, role, key, null); + } + + @SuppressWarnings("unchecked") + private C wrapForAudit(C context, KeyUsage role) { + return (C) switch (context) { + case SignatureContext signatureContext -> AuditedContexts.wrap(signatureContext, auditSink, role); + case EncryptionContext encryptionContext -> AuditedContexts.wrap(encryptionContext, auditSink, role); + case KemContext kemContext -> AuditedContexts.wrap(kemContext, auditSink, role); + case DigestContext digestContext -> AuditedContexts.wrap(digestContext, auditSink, role); + case MacContext macContext -> AuditedContexts.wrap(macContext, auditSink, role); + case AgreementContext agreementContext -> AuditedContexts.wrap(agreementContext, auditSink, role); + }; + } + + /** + * Destroys a key and verifies that it entered the destroyed state. + * + * @param algorithmId algorithm identifier used as audit metadata + * @param provider provider name used as audit metadata + * @param key key to destroy; must not be {@code null} + * @return {@code true} only when this call transitions the key to destroyed; + * {@code false} for a non-destroyable or already destroyed key + * @throws NullPointerException if {@code key} is {@code null} + * @throws DestroyFailedException if destruction fails or the key does not + * report itself destroyed afterward + * @throws RuntimeException if the key's lifecycle implementation throws one + */ + public boolean destroyKey(String algorithmId, String provider, Key key) throws DestroyFailedException { + Objects.requireNonNull(key, "key must not be null"); + if (!(key instanceof Destroyable destroyable)) { + return false; + } + ReentrantLock destroyLock = DESTROY_LOCKS[Math.floorMod(System.identityHashCode(key), DESTROY_LOCKS.length)]; + destroyLock.lock(); + try { + if (destroyable.isDestroyed()) { + return false; + } + destroyable.destroy(); + if (!destroyable.isDestroyed()) { + throw new DestroyFailedException("Key did not enter the destroyed state"); + } + } finally { + destroyLock.unlock(); + } + auditSink.onKeyDestroyed(algorithmId, provider, key); + return true; + } + + private void notifyContextCreated(CryptoAlgorithm algorithm, + KeyUsage role, S spec) { + Map metadata = spec == null ? Map.of() + : Map.of("specType", spec.getClass().getName()); + auditSink.onContextCreatedMeta(UUID.randomUUID().toString(), algorithm.id(), algorithm.providerName(), + role, "n/a", metadata); + } + + /* default */ void notifyKeyPairGenerated(CryptoAlgorithm algorithm, AlgorithmKeySpec spec, KeyPair keyPair) { + auditSink.onKeyGenerated(algorithm.id(), algorithm.providerName(), spec, keyPair); + } + + /* default */ void notifyKeyGenerated(CryptoAlgorithm algorithm, AlgorithmKeySpec spec, Key key) { + notifyKeyBuilt(algorithm, spec, key); + } + + /* default */ void notifyKeyBuilt(CryptoAlgorithm algorithm, AlgorithmKeySpec spec, Key key) { + auditSink.onKeyBuilt(algorithm.id(), algorithm.providerName(), spec, key); + } + + private static ReentrantLock[] createDestroyLocks() { + ReentrantLock[] locks = new ReentrantLock[DESTROY_LOCK_STRIPES]; + for (int index = 0; index < locks.length; index++) { + locks[index] = new ReentrantLock(); + } + return locks; + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/HybridKexBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/HybridKexBuilder.java index c5e3791..9b399ed 100644 --- a/lib/src/main/java/zeroecho/sdk/builders/HybridKexBuilder.java +++ b/lib/src/main/java/zeroecho/sdk/builders/HybridKexBuilder.java @@ -33,12 +33,13 @@ ******************************************************************************/ package zeroecho.sdk.builders; +import zeroecho.sdk.ZeroEchoSession; + import java.io.IOException; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Objects; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.KeyUsage; import zeroecho.core.alg.common.agreement.KeyPairKey; import zeroecho.core.context.AgreementContext; @@ -89,6 +90,7 @@ import zeroecho.sdk.hybrid.kex.HybridKexTranscript; */ public final class HybridKexBuilder { + private final ZeroEchoSession session; private HybridKexProfile profile; private HybridKexTranscript transcript; private HybridKexPolicy policy; @@ -106,17 +108,19 @@ public final class HybridKexBuilder { private PublicKey pqcPeerPublic; private PrivateKey pqcPrivate; - private HybridKexBuilder() { - // builder + private HybridKexBuilder(ZeroEchoSession session) { + this.session = Objects.requireNonNull(session, "session"); } /** * Creates a new builder instance. * + * @param session explicit runtime configuration * @return new builder + * @throws NullPointerException if {@code session} is {@code null} */ - public static HybridKexBuilder builder() { - return new HybridKexBuilder(); + public static HybridKexBuilder builder(ZeroEchoSession session) { + return new HybridKexBuilder(session); } /** @@ -245,26 +249,23 @@ public final class HybridKexBuilder { * * @return classic agreement context derived from the configured classic-leg * state - * @throws IOException if underlying context creation fails * @throws IllegalStateException if the selected classic mode is missing * required state */ - private AgreementContext buildClassicLeg() throws IOException { + private AgreementContext buildClassicLeg() { if (classicMode == ClassicMode.CLASSIC_AGREEMENT) { - if (classicPrivate == null || classicPeerPublic == null) { - throw new IllegalStateException( - "classic private key and peer public must be set for CLASSIC_AGREEMENT"); - } - AgreementContext classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicPrivate, + AgreementContext classic = session.createContext(classicAlgId, KeyUsage.AGREEMENT, classicPrivate, classicSpec); - classic.setPeerPublic(classicPeerPublic); - return classic; + try { + classic.setPeerPublic(classicPeerPublic); + return classic; + } catch (RuntimeException | Error failure) { // NOPMD - close context on unchecked failure + closeAfterFailure(classic, failure); + throw failure; + } } if (classicMode == ClassicMode.PAIR_MESSAGE) { - if (classicKeyPair == null) { - throw new IllegalStateException("classic key pair must be set for PAIR_MESSAGE"); - } - return CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicKeyPair, classicSpec); + return session.createContext(classicAlgId, KeyUsage.AGREEMENT, classicKeyPair, classicSpec); } throw new IllegalStateException("classic mode must be selected"); } @@ -273,46 +274,48 @@ public final class HybridKexBuilder { * Builds initiator-side context. * * @return initiator context - * @throws IOException if underlying context creation fails */ - public HybridKexContext buildInitiator() throws IOException { - validateCommon(); - - AgreementContext classic = buildClassicLeg(); - - if (pqcPeerPublic == null) { - throw new IllegalStateException("pqc peer public must be set for initiator"); - } - MessageAgreementContext pqc = CryptoAlgorithms.create(pqcAlgId, KeyUsage.AGREEMENT, pqcPeerPublic, pqcSpec); - + public HybridKexContext buildInitiator() { + validateInitiator(); HybridKexProfile effective = effectiveProfile(); - if (policy != null) { - policy.enforce(effective, classic, pqc); + AgreementContext classic = null; + MessageAgreementContext pqc = null; + try { + classic = buildClassicLeg(); + pqc = session.createContext(pqcAlgId, KeyUsage.AGREEMENT, pqcPeerPublic, pqcSpec); + if (policy != null) { + policy.enforce(effective, classic, pqc); + } + return new HybridKexContext(effective, classic, pqc); + } catch (RuntimeException | Error failure) { // NOPMD - close partial construction + closeAfterFailure(pqc, failure); + closeAfterFailure(classic, failure); + throw failure; } - return new HybridKexContext(effective, classic, pqc); } /** * Builds responder-side context. * * @return responder context - * @throws IOException if underlying context creation fails */ - public HybridKexContext buildResponder() throws IOException { - validateCommon(); - - AgreementContext classic = buildClassicLeg(); - - if (pqcPrivate == null) { - throw new IllegalStateException("pqc private key must be set for responder"); - } - MessageAgreementContext pqc = CryptoAlgorithms.create(pqcAlgId, KeyUsage.AGREEMENT, pqcPrivate, pqcSpec); - + public HybridKexContext buildResponder() { + validateResponder(); HybridKexProfile effective = effectiveProfile(); - if (policy != null) { - policy.enforce(effective, classic, pqc); + AgreementContext classic = null; + MessageAgreementContext pqc = null; + try { + classic = buildClassicLeg(); + pqc = session.createContext(pqcAlgId, KeyUsage.AGREEMENT, pqcPrivate, pqcSpec); + if (policy != null) { + policy.enforce(effective, classic, pqc); + } + return new HybridKexContext(effective, classic, pqc); + } catch (RuntimeException | Error failure) { // NOPMD - close partial construction + closeAfterFailure(pqc, failure); + closeAfterFailure(classic, failure); + throw failure; } - return new HybridKexContext(effective, classic, pqc); } /** @@ -340,6 +343,39 @@ public final class HybridKexBuilder { if (pqcAlgId == null) { throw new IllegalStateException("pqc algorithm id must be set"); } + if (classicMode == ClassicMode.CLASSIC_AGREEMENT + && (classicPrivate == null || classicPeerPublic == null)) { + throw new IllegalStateException( + "classic private key and peer public must be set for CLASSIC_AGREEMENT"); + } + if (classicMode == ClassicMode.PAIR_MESSAGE && classicKeyPair == null) { + throw new IllegalStateException("classic key pair must be set for PAIR_MESSAGE"); + } + } + + private void validateInitiator() { + validateCommon(); + if (pqcPeerPublic == null) { + throw new IllegalStateException("pqc peer public must be set for initiator"); + } + } + + private void validateResponder() { + validateCommon(); + if (pqcPrivate == null) { + throw new IllegalStateException("pqc private key must be set for responder"); + } + } + + private static void closeAfterFailure(zeroecho.core.context.CryptoContext context, Throwable failure) { + if (context == null) { + return; + } + try { + context.close(); + } catch (IOException | RuntimeException closeFailure) { // NOPMD - preserve close failure + failure.addSuppressed(closeFailure); + } } private HybridKexProfile effectiveProfile() { @@ -693,12 +729,11 @@ public final class HybridKexBuilder { * configuration. * * @return initiator context - * @throws IOException if underlying context creation fails * @throws IllegalStateException if required configuration for initiator role is * missing * @since 1.0 */ - public HybridKexContext buildInitiator() throws IOException { + public HybridKexContext buildInitiator() { return parent.buildInitiator(); } @@ -707,12 +742,11 @@ public final class HybridKexBuilder { * configuration. * * @return responder context - * @throws IOException if underlying context creation fails * @throws IllegalStateException if required configuration for responder role is * missing * @since 1.0 */ - public HybridKexContext buildResponder() throws IOException { + public HybridKexContext buildResponder() { return parent.buildResponder(); } } diff --git a/lib/src/main/java/zeroecho/sdk/builders/SignatureTrailerDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/SignatureTrailerDataContentBuilder.java index 28fb945..b26119b 100644 --- a/lib/src/main/java/zeroecho/sdk/builders/SignatureTrailerDataContentBuilder.java +++ b/lib/src/main/java/zeroecho/sdk/builders/SignatureTrailerDataContentBuilder.java @@ -33,7 +33,8 @@ ******************************************************************************/ package zeroecho.sdk.builders; -import java.io.IOException; +import zeroecho.sdk.ZeroEchoSession; + import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; @@ -42,7 +43,6 @@ import java.util.function.Supplier; import conflux.CtxInterface; import conflux.Key; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.KeyUsage; import zeroecho.core.spec.ContextSpec; import zeroecho.core.tag.TagEngine; @@ -75,19 +75,15 @@ import zeroecho.sdk.hybrid.signature.HybridSignatureProfile; *
        *
      • {@link #core(TagEngine)} / {@link #core(Supplier)}: wraps a ready engine * (same parameters as {@link TagTrailerDataContentBuilder}).
      • - *
      • {@link #single()}: constructs a non-hybrid {@code SignatureContext} via - * {@link CryptoAlgorithms}.
      • - *
      • {@link #hybrid()}: constructs a hybrid {@code SignatureContext} via + *
      • {@link #single(ZeroEchoSession)}: constructs a non-hybrid + * {@code SignatureContext}.
      • + *
      • {@link #hybrid(ZeroEchoSession)}: constructs a hybrid + * {@code SignatureContext} via * {@link HybridSignatureContexts}.
      • *
      * - *

      Checked exceptions

      - *

      - * Context construction may involve I/O (e.g., catalog/provider loading) and - * therefore throw {@link IOException}. This builder converts such failures to - * {@link IllegalStateException} because fluent builder APIs are expected to be - * used in configuration code without mandatory checked-exception plumbing. - *

      + *

      Context construction is in-memory. Checked I/O failures arise only when a + * built stream is attached or processed.

      * * @since 1.0 */ @@ -146,8 +142,8 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil * @return selector for creating signing/verifying builders * @since 1.0 */ - public static SingleSelector single() { - return new SingleSelector(); + public static SingleSelector single(ZeroEchoSession session) { + return new SingleSelector(session); } /** @@ -156,8 +152,8 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil * @return selector for creating signing/verifying builders * @since 1.0 */ - public static HybridSelector hybrid() { - return new HybridSelector(); + public static HybridSelector hybrid(ZeroEchoSession session) { + return new HybridSelector(session); } /** @@ -218,7 +214,10 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil */ public static final class SingleSelector { - private SingleSelector() { + private final ZeroEchoSession session; + + private SingleSelector(ZeroEchoSession session) { + this.session = Objects.requireNonNull(session, "session"); } /** @@ -255,13 +254,8 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil Objects.requireNonNull(algorithmId, "algorithmId"); Objects.requireNonNull(privateKey, "privateKey"); - Supplier> factory = () -> { - try { - return CryptoAlgorithms.create(algorithmId, KeyUsage.SIGN, privateKey, spec); - } catch (IOException e) { - throw new IllegalStateException("Failed to create SIGN SignatureContext for: " + algorithmId, e); - } - }; + Supplier> factory = () -> session.createContext(algorithmId, KeyUsage.SIGN, + privateKey, spec); return core(factory); } @@ -300,13 +294,8 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil Objects.requireNonNull(algorithmId, "algorithmId"); Objects.requireNonNull(publicKey, "publicKey"); - Supplier> factory = () -> { - try { - return CryptoAlgorithms.create(algorithmId, KeyUsage.VERIFY, publicKey, spec); - } catch (IOException e) { - throw new IllegalStateException("Failed to create VERIFY SignatureContext for: " + algorithmId, e); - } - }; + Supplier> factory = () -> session.createContext(algorithmId, + KeyUsage.VERIFY, publicKey, spec); return core(factory); } @@ -324,8 +313,10 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil public static final class HybridSelector { private static final int DEFAULT_MAX_BODY_BYTES = 2 * 1024 * 1024; + private final ZeroEchoSession session; - private HybridSelector() { + private HybridSelector(ZeroEchoSession session) { + this.session = Objects.requireNonNull(session, "session"); } /** @@ -402,7 +393,7 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil Supplier> factory = () -> { try { - return HybridSignatureContexts.sign(profile, classicPrivate, pqcPrivate, maxBodyBytes); + return HybridSignatureContexts.sign(session, profile, classicPrivate, pqcPrivate, maxBodyBytes); } catch (RuntimeException e) { // NOPMD throw e; } catch (Exception e) { @@ -449,7 +440,7 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil Supplier> factory = () -> { try { - return HybridSignatureContexts.verify(profile, classicPublic, pqcPublic, maxBodyBytes); + return HybridSignatureContexts.verify(session, profile, classicPublic, pqcPublic, maxBodyBytes); } catch (RuntimeException e) { // NOPMD throw e; } catch (Exception e) { diff --git a/lib/src/main/java/zeroecho/sdk/builders/TagTrailerDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/TagTrailerDataContentBuilder.java index b01b570..deaec72 100644 --- a/lib/src/main/java/zeroecho/sdk/builders/TagTrailerDataContentBuilder.java +++ b/lib/src/main/java/zeroecho/sdk/builders/TagTrailerDataContentBuilder.java @@ -108,9 +108,9 @@ public final class TagTrailerDataContentBuilder implements DataContentBuilder * Creates a builder bound to a fixed engine instance. * *

      - * This constructor is backward compatible but ties the builder to a single-use - * engine. Prefer {@link #TagTrailerDataContentBuilder(Supplier)} when multiple - * streams are expected. + * This form ties the builder to a single-use engine. Prefer + * {@link #TagTrailerDataContentBuilder(Supplier)} when multiple streams are + * expected. *

      * * @param engine preconstructed engine instance; must not be {@code null} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/AbstractStreamingSignatureDataBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/AbstractStreamingSignatureDataBuilder.java deleted file mode 100644 index 27d9026..0000000 --- a/lib/src/main/java/zeroecho/sdk/builders/alg/AbstractStreamingSignatureDataBuilder.java +++ /dev/null @@ -1,1302 +0,0 @@ -/******************************************************************************* - * Copyright (C) 2026, Leo Galambos - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software must - * display the following acknowledgement: - * This product includes software developed by the Egothor project. - * - * 4. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ******************************************************************************/ -package zeroecho.sdk.builders.alg; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.util.Base64; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import conflux.CtxInterface; -import conflux.Key; -import zeroecho.core.CryptoAlgorithm; -import zeroecho.core.CryptoAlgorithms; -import zeroecho.core.context.SignatureContext; -import zeroecho.core.spec.AlgorithmKeySpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; -import zeroecho.core.tag.SignatureVerificationStrategy; -import zeroecho.sdk.builders.core.DataContentBuilder; -import zeroecho.sdk.content.api.DataContent; -import zeroecho.sdk.content.api.PlainContent; -import zeroecho.sdk.io.SignatureTrailerInputStream; - -/** - * A reusable streaming signature builder that signs or verifies data as it - * flows through an {@link java.io.InputStream}. - * - *

      - * This abstract builder composes {@link PlainContent} pipelines that either - * produce a signature while streaming the original input (sign passthrough), - * emit the signature itself in various encodings, or verify an expected - * signature while streaming or emit a boolean result. Algorithms are supplied - * by {@link CryptoAlgorithms}; concrete subclasses provide algorithm-specific - * details through protected abstract hooks. - *

      - * - *

      What this builder does

      - *
        - *
      • Obtains an algorithm instance and keys (direct, imported, or - * generated).
      • - *
      • Creates a {@link SignatureContext} in SIGN or VERIFY mode on demand.
      • - *
      • Builds a streaming pipeline that either passes data through or emits a - * detached artifact.
      • - *
      • Lets callers plug in a verification approach (constant-time compare, - * throw-on-mismatch, flag-in-context, etc.).
      • - *
      - * - *

      Typical usage

      {@code
      - * // Signing while passing the original bytes downstream:
      - * PlainContent content = new MyAlgStreamingSignatureDataBuilder()
      - *     .sign()
      - *     .withPrivateKey(privateKey)
      - *     .passThrough()
      - *     .build(true);
      - *
      - * // Emitting a hex-encoded detached signature:
      - * PlainContent sigOut = new MyAlgStreamingSignatureDataBuilder()
      - *     .sign()
      - *     .withPrivateKey(privateKey)
      - *     .emitHexSignature()
      - *     .build(true);
      - *
      - * // Verifying against an expected signature and passing data through:
      - * PlainContent verified = new MyAlgStreamingSignatureDataBuilder()
      - *     .verify()
      - *     .withPublicKey(publicKey)
      - *     .expectedSignatureBase64(b64Sig)
      - *     .passThrough()
      - *     .build(true);
      - *
      - * // Verifying and emitting a boolean ("true" or "false"):
      - * PlainContent ok = new MyAlgStreamingSignatureDataBuilder()
      - *     .verify()
      - *     .emitVerificationBoolean()
      - *     .withPublicKey(publicKey)
      - *     .expectedSignature(rawSig)
      - *     .build(true);
      - * }
      - * - *

      - * The type parameters represent algorithm-specific key specifications: - *

      - *
        - *
      • {@code KG} - key generation specification type
      • - *
      • {@code PUB} - public key import specification type
      • - *
      • {@code PRIV} - private key import specification type
      • - *
      - * - *

      - * Subclasses supply algorithm name, key spec classes, default key generation - * supplier, factories for import specs, and creation of - * {@link SignatureContext} instances for signing and verification. - *

      - * - *

      Thread-safety

      - *

      - * Builders are mutable and not thread-safe. Create and use an instance on a - * single thread. - *

      - * - * @param key generation spec type for the algorithm - * @param public key import spec type for the algorithm - * @param private key import spec type for the algorithm - */ -public abstract class AbstractStreamingSignatureDataBuilder - implements DataContentBuilder { - private Mode mode = Mode.SIGN; - - private PrivateKey privateKey; - private PublicKey publicKey; - - private boolean genKeyPair; - - private byte[] _importPrivatePkcs8; - private String importPrivateProvider; // optional - - private byte[] _importPublicX509; - private String importPublicProvider; // optional - - private byte[] _expectedSignature; // VERIFY: raw - private Key _expectedSignatureFromCtx; // optional ctx fetch - - private Output out = Output.PASSTHROUGH; - - private CtxInterface ctx; // optional - private Key storeSigKey; // optional - private SignatureVerificationStrategy _strategy; // = null optional - - private Consumer sigCallback; // optional - - private int _bufferSize = 8192; - - private CryptoAlgorithm algorithm; // resolved in resolveKeys() - - /** - * Returns the canonical algorithm name used to resolve an implementation from - * {@link CryptoAlgorithms}. - * - *

      - * Examples include "Ed25519" or "SPHINCS+". - *

      - * - * @return the algorithm name understood by - * {@link CryptoAlgorithms#require(String)} - */ - protected abstract String algorithmName(); - - /** - * Creates a {@link SignatureContext} in sign mode for the given algorithm and - * private key. - * - *

      - * Implementations should instantiate a signing context configured for streaming - * updates and producing the final signature tag. - *

      - * - * @param alg the resolved algorithm instance - * @param key the private key used to generate signatures - * @return a new signature context in sign mode - * @throws GeneralSecurityException if the context cannot be created for the - * provided algorithm or key - */ - protected abstract SignatureContext newSignContext(CryptoAlgorithm alg, PrivateKey key) - throws GeneralSecurityException; - - /** - * Creates a {@link SignatureContext} in verify mode for the given algorithm and - * public key. - * - *

      - * Implementations should instantiate a verification context configured for - * streaming updates and validating the final signature tag. - *

      - * - * @param alg the resolved algorithm instance - * @param key the public key used to verify signatures - * @return a new signature context in verify mode - * @throws GeneralSecurityException if the context cannot be created for the - * provided algorithm or key - */ - protected abstract SignatureContext newVerifyContext(CryptoAlgorithm alg, PublicKey key) - throws GeneralSecurityException; - - /** - * Returns the key generation specification class for the algorithm. - * - * @return the class object representing {@code KG} - */ - protected abstract Class keyGenSpecClass(); - - /** - * Returns the public key import specification class for the algorithm. - * - * @return the class object representing {@code PUB} - */ - protected abstract Class publicKeySpecClass(); - - /** - * Returns the private key import specification class for the algorithm. - * - * @return the class object representing {@code PRIV} - */ - protected abstract Class privateKeySpecClass(); - - /** - * Returns a supplier of default key generation specifications. - * - *

      - * The supplier must not return null when invoked. - *

      - * - * @return a non-null supplier of default {@code KG} instances - */ - protected abstract Supplier defaultKeyGenSpecSupplier(); - - /** - * Returns the currently configured key generation specification or null if the - * default should be used. - * - *

      - * Subclasses typically provide a public setter to allow users to set a - * non-default specification. - *

      - * - * @return the current key generation spec or null to indicate default should be - * used - */ - protected abstract KG currentKeyGenSpecOrNull(); - - /** - * Builds a public key import specification from X.509-encoded bytes. - * - *

      - * The {@code providerHint} may be ignored if not applicable to the - * implementation. - *

      - * - * @param x509 the X.509 SubjectPublicKeyInfo bytes - * @param providerHint an optional provider name hint, may be null - * @return the public key import spec instance - */ - protected abstract PUB makePublicKeySpec(byte[] x509, String providerHint); - - /** - * Builds a private key import specification from PKCS#8-encoded bytes. - * - *

      - * The {@code providerHint} may be ignored if not applicable to the - * implementation. - *

      - * - * @param pkcs8 the PKCS#8 PrivateKeyInfo bytes - * @param providerHint an optional provider name hint, may be null - * @return the private key import spec instance - */ - protected abstract PRIV makePrivateKeySpec(byte[] pkcs8, String providerHint); - - /** - * Returns the default provider name hint used when importing keys if no - * explicit provider was set. - * - * @return the provider hint or null if there is no preference - */ - protected abstract String defaultProviderHint(); - - /** - * Operating mode for the builder. - */ - public enum Mode { - /** - * Sign mode produces a signature using a private key. - */ - SIGN, - /** - * Verify mode checks an expected signature using a public key. - */ - VERIFY - } - - /** - * Declares the output mode for a cryptographic operation. - * - *

      - * Each constant specifies how the result of an operation such as signing, - * verification, or transformation should be returned or rendered. - *

      - * - *

      Modes

      - *
        - *
      • {@link #PASSTHROUGH} - Return the original data without - * modification.
      • - *
      • {@link #SIG_RAW} - Return the raw signature bytes produced by the - * algorithm.
      • - *
      • {@link #SIG_HEX} - Return the signature encoded as a hexadecimal - * string.
      • - *
      • {@link #SIG_BASE64} - Return the signature encoded in Base64.
      • - *
      • {@link #VERIFY_BOOL} - Return a boolean result of verification - * ({@code true} if valid, {@code false} otherwise).
      • - *
      - * - *

      Thread-safety

      Enum constants are immutable and inherently - * thread-safe. - */ - private enum Output { - /** Return the original data without modification. */ - PASSTHROUGH, - /** Return raw signature bytes. */ - SIG_RAW, - /** Return the signature as a hexadecimal string. */ - SIG_HEX, - /** Return the signature as a Base64-encoded string. */ - SIG_BASE64, - /** Return a boolean verification result. */ - VERIFY_BOOL - } - - /** - * Switches the builder to sign mode. - * - * @return {@code this} builder for chaining - */ - public AbstractStreamingSignatureDataBuilder sign() { - this.mode = Mode.SIGN; - return this; - } - - /** - * Switches the builder to verify mode. - * - * @return {@code this} builder for chaining - */ - public AbstractStreamingSignatureDataBuilder verify() { - this.mode = Mode.VERIFY; - return this; - } - - /** - * Configures the output to pass the original data through unchanged. - * - *

      - * In sign mode this computes the signature but appends it only when using the - * internal trailer format. In verify mode this verifies as the data is consumed - * while passing it through. - *

      - * - * @return {@code this} builder for chaining - */ - public AbstractStreamingSignatureDataBuilder passThrough() { - this.out = Output.PASSTHROUGH; - return this; - } - - /** - * Configures the output to emit the raw detached signature bytes. - * - * @return {@code this} builder for chaining - */ - public AbstractStreamingSignatureDataBuilder emitRawSignature() { - this.out = Output.SIG_RAW; - return this; - } - - /** - * Configures the output to emit the detached signature as lowercase hexadecimal - * text. - * - * @return {@code this} builder for chaining - */ - public AbstractStreamingSignatureDataBuilder emitHexSignature() { - this.out = Output.SIG_HEX; - return this; - } - - /** - * Configures the output to emit the detached signature as Base64 text. - * - * @return {@code this} builder for chaining - */ - public AbstractStreamingSignatureDataBuilder emitBase64Signature() { - this.out = Output.SIG_BASE64; - return this; - } - - /** - * Configures the builder to verify and emit a boolean result encoded as ASCII - * "true" or "false". - * - *

      - * This method also switches the mode to {@link Mode#VERIFY}. - *

      - * - * @return {@code this} builder for chaining - */ - public AbstractStreamingSignatureDataBuilder emitVerificationBoolean() { - this.mode = Mode.VERIFY; - this.out = Output.VERIFY_BOOL; - return this; - } - - /** - * Sets the internal streaming buffer size used when consuming input. - * - * @param bytes buffer size in bytes, must be greater than or equal to 1 - * @return {@code this} builder for chaining - * @throws IllegalArgumentException if {@code bytes < 1} - */ - public AbstractStreamingSignatureDataBuilder bufferSize(int bytes) { - if (bytes < 1) { // NOPMD - throw new IllegalArgumentException("bufferSize must be >= 1"); - } - this._bufferSize = bytes; - return this; - } - - /** - * Sets the private key to be used in sign mode. - * - * @param k the private key, must not be null - * @return {@code this} builder for chaining - * @throws NullPointerException if {@code k} is null - */ - public AbstractStreamingSignatureDataBuilder withPrivateKey(PrivateKey k) { - this.privateKey = Objects.requireNonNull(k); - return this; - } - - /** - * Sets the public key to be used in verify mode. - * - * @param k the public key, must not be null - * @return {@code this} builder for chaining - * @throws NullPointerException if {@code k} is null - */ - public AbstractStreamingSignatureDataBuilder withPublicKey(PublicKey k) { - this.publicKey = Objects.requireNonNull(k); - return this; - } - - /** - * Requests generation of a fresh key pair using the algorithm-specific key - * generation spec. - * - *

      - * If called, a key pair will be generated during {@link #build(boolean)} using - * either the current key generation spec or a default one from - * {@link #defaultKeyGenSpecSupplier()}. - *

      - * - * @return {@code this} builder for chaining - */ - public AbstractStreamingSignatureDataBuilder generateKeyPair() { - this.genKeyPair = true; - return this; - } - - /** - * Provides a PKCS#8-encoded private key to import using the default provider - * hint. - * - * @param pkcs8 PKCS#8 PrivateKeyInfo bytes, must not be null - * @return {@code this} builder for chaining - * @throws NullPointerException if {@code pkcs8} is null - */ - public AbstractStreamingSignatureDataBuilder importPrivatePkcs8(byte[] pkcs8) { - this._importPrivatePkcs8 = Objects.requireNonNull(pkcs8).clone(); - this.importPrivateProvider = null; - return this; - } - - /** - * Provides a PKCS#8-encoded private key to import using the given provider - * name. - * - * @param pkcs8 PKCS#8 PrivateKeyInfo bytes, must not be null - * @param providerName provider name hint to use, may be null - * @return {@code this} builder for chaining - * @throws NullPointerException if {@code pkcs8} is null - */ - public AbstractStreamingSignatureDataBuilder importPrivatePkcs8(byte[] pkcs8, String providerName) { - this._importPrivatePkcs8 = Objects.requireNonNull(pkcs8).clone(); - this.importPrivateProvider = providerName; - return this; - } - - /** - * Provides an X.509-encoded public key to import using the default provider - * hint. - * - * @param x509 X.509 SubjectPublicKeyInfo bytes, must not be null - * @return {@code this} builder for chaining - * @throws NullPointerException if {@code x509} is null - */ - public AbstractStreamingSignatureDataBuilder importPublicX509(byte[] x509) { - this._importPublicX509 = Objects.requireNonNull(x509).clone(); - this.importPublicProvider = null; - return this; - } - - /** - * Provides an X.509-encoded public key to import using the given provider name. - * - * @param x509 X.509 SubjectPublicKeyInfo bytes, must not be null - * @param providerName provider name hint to use, may be null - * @return {@code this} builder for chaining - * @throws NullPointerException if {@code x509} is null - */ - public AbstractStreamingSignatureDataBuilder importPublicX509(byte[] x509, String providerName) { - this._importPublicX509 = Objects.requireNonNull(x509).clone(); - this.importPublicProvider = providerName; - return this; - } - - /** - * Sets the expected signature for verification as raw bytes. - * - * @param raw the expected signature bytes, must not be null - * @return {@code this} builder for chaining - * @throws NullPointerException if {@code raw} is null - */ - public AbstractStreamingSignatureDataBuilder expectedSignature(byte[] raw) { - this._expectedSignature = Objects.requireNonNull(raw).clone(); - return this; - } - - /** - * Sets the expected signature for verification from a hexadecimal string. - * - * @param hex lowercase or uppercase hexadecimal string, must not be null - * @return {@code this} builder for chaining - * @throws NullPointerException if {@code hex} is null - * @throws IllegalArgumentException if {@code hex} is not valid hexadecimal - */ - public AbstractStreamingSignatureDataBuilder expectedSignatureHex(String hex) { - this._expectedSignature = java.util.HexFormat.of().parseHex(Objects.requireNonNull(hex)); - return this; - } - - /** - * Sets the expected signature for verification from a Base64 string. - * - * @param b64 Base64-encoded signature text, must not be null - * @return {@code this} builder for chaining - * @throws NullPointerException if {@code b64} is null - * @throws IllegalArgumentException if {@code b64} is not valid Base64 - */ - public AbstractStreamingSignatureDataBuilder expectedSignatureBase64(String b64) { - this._expectedSignature = Base64.getDecoder().decode(Objects.requireNonNull(b64)); - return this; - } - - /** - * Configures verification to fetch the expected signature bytes from a context - * when building the stream. - * - * @param key the context key under which the expected signature is stored - * @return {@code this} builder for chaining - * @throws NullPointerException if {@code key} is null - */ - public AbstractStreamingSignatureDataBuilder expectedSignatureFromCtx(Key key) { - this._expectedSignatureFromCtx = Objects.requireNonNull(key); - return this; - } - - /** - * Sets the optional runtime context to read or write auxiliary values such as a - * generated signature or verification result. - * - * @param c the context instance, may be null to disable context integration - * @return {@code this} builder for chaining - */ - public AbstractStreamingSignatureDataBuilder context(CtxInterface c) { - this.ctx = c; - return this; - } - - /** - * Configures the context key under which a generated signature will be stored - * after signing. - * - * @param key the context key to store the signature under, may be null to - * disable storage - * @return {@code this} builder for chaining - */ - public AbstractStreamingSignatureDataBuilder storeSignature(Key key) { - this.storeSigKey = key; - return this; - } - - /** - * Registers a callback that receives the generated signature bytes after - * signing completes. - * - * @param cb the callback to invoke with a defensive copy of the signature, may - * be null - * @return {@code this} builder for chaining - */ - public AbstractStreamingSignatureDataBuilder onSignature(Consumer cb) { - this.sigCallback = cb; - return this; - } - - /** - * Sets a custom verification approach to be applied by verify-mode pipelines. - * - *

      - * The strategy defines how the computed and expected tags are compared and how - * failures are surfaced. For example, callers may supply - * {@code getVerificationCore().getThrowOnMismatch()} to raise on mismatch, or a - * decorated variant that records a boolean flag in a context. - *

      - * - *

      Default behavior

      - *

      - * If this method is not called, verify-mode pipelines use the default core with - * throw-on-mismatch semantics. - *

      - * - * @param strategy verification strategy; if {@code null}, the default core is - * used - * @return {@code this} builder for chaining - */ - public AbstractStreamingSignatureDataBuilder withStrategy(SignatureVerificationStrategy strategy) { - this._strategy = strategy; - return this; - } - - /** - * Builds the configured streaming signature pipeline as {@link PlainContent}. - * - *

      - * This method resolves the algorithm and keys according to the current - * configuration, then returns a {@link PlainContent} that will perform signing - * or verification when its stream is consumed. - *

      - * - *

      - * The boolean parameter is ignored and present only to satisfy the - * {@link DataContentBuilder} interface. - *

      - * - *
      {@code
      -     * PlainContent pipeline = builder
      -     *     .sign()
      -     *     .withPrivateKey(pk)
      -     *     .passThrough()
      -     *     .build(true);
      -     * }
      - * - * @param ignored not used - * @return a {@link PlainContent} instance that performs the requested operation - * on stream consumption - * @throws IllegalStateException if required keys are missing for the selected - * mode or if key setup fails - */ - @Override - public PlainContent build(boolean ignored) { - try { - resolveKeys(); - } catch (GeneralSecurityException e) { - throw new IllegalStateException(algorithmName() + " key setup failed", e); - } - return switch (mode) { - case SIGN -> { - if (privateKey == null) { - throw new IllegalStateException("SIGN mode needs a PrivateKey"); - } - yield (out == Output.PASSTHROUGH) - ? new SignPassthrough(algorithm, privateKey, ctx, storeSigKey, sigCallback, _bufferSize) - : new SignEmit(algorithm, privateKey, out, ctx, storeSigKey, sigCallback, _bufferSize); - } - case VERIFY -> { - if (publicKey == null) { - throw new IllegalStateException("VERIFY mode needs a PublicKey"); - } - yield (out == Output.VERIFY_BOOL) - ? new VerifyEmit(algorithm, publicKey, _expectedSignature, _expectedSignatureFromCtx, ctx, - _strategy) - : new VerifyPassthrough(algorithm, publicKey, _expectedSignature, _expectedSignatureFromCtx, - ctx, _strategy); - } - }; - } - - private void resolveKeys() throws GeneralSecurityException { - this.algorithm = CryptoAlgorithms.require(algorithmName()); - - if (genKeyPair) { - final Supplier sup = Objects.requireNonNull(defaultKeyGenSpecSupplier(), "defaultKeyGenSpecSupplier"); - final KG spec = (currentKeyGenSpecOrNull() != null) ? currentKeyGenSpecOrNull() : sup.get(); - final AsymmetricKeyBuilder b = algorithm.asymmetricKeyBuilder(keyGenSpecClass()); - final KeyPair kp = b.generateKeyPair(spec); - this.privateKey = kp.getPrivate(); - this.publicKey = kp.getPublic(); - } - if (_importPrivatePkcs8 != null) { - final String prov = (importPrivateProvider != null) ? importPrivateProvider : defaultProviderHint(); - final PRIV privSpec = makePrivateKeySpec(_importPrivatePkcs8, prov); - final AsymmetricKeyBuilder b = algorithm.asymmetricKeyBuilder(privateKeySpecClass()); - this.privateKey = b.importPrivate(privSpec); - } - if (_importPublicX509 != null) { - final String prov = (importPublicProvider != null) ? importPublicProvider : defaultProviderHint(); - final PUB pubSpec = makePublicKeySpec(_importPublicX509, prov); - final AsymmetricKeyBuilder b = algorithm.asymmetricKeyBuilder(publicKeySpecClass()); - this.publicKey = b.importPublic(pubSpec); - } - } - - /** - * Pass-through content that signs the streamed bytes and emits the final - * signature. - * - *

      - * {@code SignPassthrough} attaches to an upstream {@link DataContent}, returns - * an {@link InputStream} that forwards all bytes unchanged, and computes a - * digital signature as the stream is consumed. When the stream reaches EOF, the - * signature is finalized and can be stored in a {@link CtxInterface} and/or - * delivered to a callback if configured. - *

      - * - *

      Behavior

      - *
        - *
      • Signing context is created lazily in {@link #getStream()} via - * {@code newSignContext(alg, key)}.
      • - *
      • The returned stream is read-only and must be consumed to EOF to produce a - * signature.
      • - *
      • Failures while storing or delivering the signature are swallowed to avoid - * disrupting the caller's read loop.
      • - *
      - */ - private final class SignPassthrough implements PlainContent { - private final CryptoAlgorithm alg; - private final PrivateKey key; - private final CtxInterface ctx; - private final Key storeKey; - private final Consumer cb; - private final int bufferSize; - private volatile DataContent upstream; // NOPMD - - /** - * Creates a new pass-through signer. - * - * @param alg the algorithm that provides a {@link SignatureContext}; - * must not be {@code null} - * @param key the private key used for signing; must not be {@code null} - * @param ctx optional context used to store the produced signature; may - * be {@code null} - * @param storeKey optional key under which the signature is stored in - * {@code ctx}; may be {@code null} - * @param cb optional callback invoked with a defensive copy of the - * signature; may be {@code null} - * @param bufferSize the internal buffer size used by the trailer stream - * @throws NullPointerException if {@code alg} or {@code key} is {@code null} - */ - private SignPassthrough(CryptoAlgorithm alg, PrivateKey key, CtxInterface ctx, Key storeKey, - Consumer cb, int bufferSize) { - this.alg = Objects.requireNonNull(alg); - this.key = Objects.requireNonNull(key); - this.ctx = ctx; - this.storeKey = storeKey; - this.cb = cb; - this.bufferSize = bufferSize; - } - - /** - * Sets the upstream content that will be passed through and signed. - * - * @param input the upstream data source; must not be {@code null} - * @throws NullPointerException if {@code input} is {@code null} - */ - @Override - public void setInput(DataContent input) { - this.upstream = Objects.requireNonNull(input); - } - - /** - * Returns a stream that forwards upstream bytes unmodified and computes a - * signature. - * - *

      - * The signature is finalized when the returned stream reaches EOF and is then: - *

      - *
        - *
      • stored into {@code ctx} under {@code storeKey} if both are non-null, - * and
      • - *
      • delivered to {@code cb} if non-null (the byte array passed to the - * callback is a clone).
      • - *
      - * - * @return an input stream that signs data while passing it through unchanged - * @throws IOException if the signing context cannot be initialized or - * if the underlying upstream stream throws an I/O - * error - * @throws NullPointerException if {@link #setInput(DataContent)} was not called - * before invocation - */ - @Override - public InputStream getStream() throws IOException { - Objects.requireNonNull(upstream, "sign: missing input"); - final SignatureContext sc; - try { - sc = newSignContext(alg, key); - } catch (GeneralSecurityException e) { - throw new IOException("Failed to init sign context", e); - } - - return new SignatureTrailerInputStream(sc, upstream.getStream(), bufferSize, sig -> { - if (ctx != null && storeKey != null) { - try { - ctx.put(storeKey, sig); - } catch (RuntimeException ignore) { // NOPMD - } - } - if (cb != null) { - try { - cb.accept(sig.clone()); - } catch (RuntimeException ignore) { // NOPMD - } - } - }); - } - } - - /** - * Eager-signing content that emits only the signature in a chosen encoding. - * - *

      - * {@code SignEmit} consumes the entire upstream {@link DataContent}, computes a - * digital signature, optionally stores and/or publishes the signature, and - * returns an {@link InputStream} over the encoded signature bytes. Unlike a - * pass-through variant, the payload is fully drained internally and is not - * forwarded to the caller; the resulting stream contains only the signature - * material in the requested format. - *

      - * - *

      Behavior

      - *
        - *
      • Only {@code SIG_*} output modes are accepted.
      • - *
      • Signing context is created in {@link #getStream()} via - * {@code newSignContext(alg, key)}.
      • - *
      • Upstream is read to EOF inside {@link #getStream()} to finalize the - * signature.
      • - *
      • On success, the signature is optionally stored and/or passed to a - * callback; side-effect failures are swallowed.
      • - *
      - */ - private final class SignEmit implements PlainContent { - private final CryptoAlgorithm alg; - private final PrivateKey key; - private final Output out; - private final CtxInterface ctx; - private final Key storeKey; - private final Consumer cb; - private final int bufferSize; - private volatile DataContent upstream; // NOPMD - - /** - * Creates a new signer that emits the signature in the requested format. - * - * @param alg the algorithm used to create a {@link SignatureContext}; - * must not be {@code null} - * @param key the private key used for signing; must not be {@code null} - * @param out the desired signature output format; must be one of - * {@link Output#SIG_RAW}, {@link Output#SIG_HEX}, or - * {@link Output#SIG_BASE64} - * @param ctx optional context used to store the produced signature; may - * be {@code null} - * @param storeKey optional key under which the signature is stored in - * {@code ctx}; may be {@code null} - * @param cb optional callback invoked with a defensive copy of the - * signature; may be {@code null} - * @param bufferSize the internal buffer size used by the trailer stream - * @throws IllegalArgumentException if {@code out} is not a {@code SIG_*} - * variant - * @throws NullPointerException if {@code alg} or {@code key} is - * {@code null} - */ - private SignEmit(CryptoAlgorithm alg, PrivateKey key, Output out, CtxInterface ctx, Key storeKey, - Consumer cb, int bufferSize) { - if (out != Output.SIG_RAW && out != Output.SIG_HEX && out != Output.SIG_BASE64) { - throw new IllegalArgumentException("SignEmit requires SIG_* output"); - } - this.alg = Objects.requireNonNull(alg); - this.key = Objects.requireNonNull(key); - this.out = out; - this.ctx = ctx; - this.storeKey = storeKey; - this.cb = cb; - this.bufferSize = bufferSize; - } - - /** - * Sets the upstream content to be read and signed. - * - * @param input the upstream data source; must not be {@code null} - * @throws NullPointerException if {@code input} is {@code null} - */ - @Override - public void setInput(DataContent input) { - this.upstream = Objects.requireNonNull(input); - } - - /** - * Drains the upstream to compute the signature and returns a stream over the - * signature bytes. - * - *

      - * The method creates a {@link SignatureContext}, reads the entire upstream - * stream to EOF in order to finalize the signature, and then returns an - * {@link InputStream} over the encoded signature according to {@link #out}: - *

      - *
        - *
      • {@link Output#SIG_RAW} - raw signature bytes,
      • - *
      • {@link Output#SIG_HEX} - hexadecimal string encoded as UTF-8 bytes,
      • - *
      • {@link Output#SIG_BASE64} - Base64-encoded bytes.
      • - *
      - * - *

      - * If provided, the signature is stored in {@code ctx} under {@code storeKey} - * and passed to {@code cb}. Both operations use a defensive copy and swallow - * runtime exceptions to avoid interrupting the primary flow. - *

      - * - * @return an input stream that yields only the encoded signature bytes - * @throws IOException if the signing context cannot be initialized, - * the upstream cannot be read, or no signature - * trailer is produced - * @throws NullPointerException if {@link #setInput(DataContent)} was not - * invoked prior to this call - */ - @Override - public InputStream getStream() throws IOException { - Objects.requireNonNull(upstream, "sign: missing input"); - - final SignatureContext sc; - try { - sc = newSignContext(alg, key); - } catch (GeneralSecurityException e) { - throw new IOException("Failed to init sign context", e); - } - - final byte[][] sigHolder = new byte[1][]; - - try (SignatureTrailerInputStream in = new SignatureTrailerInputStream(sc, upstream.getStream(), bufferSize, - new Consumer<>() { - @Override - public void accept(byte[] sig) { - sigHolder[0] = (sig == null ? null : sig.clone()); - } - })) { - in.transferTo(OutputStream.nullOutputStream()); - } catch (IOException ioe) { - try { - sc.close(); - } catch (RuntimeException ignore) { // NOPMD - } - throw ioe; - } - - final byte[] sig = sigHolder[0]; - if (sig == null) { - throw new IOException("Missing signature trailer"); - } - - if (ctx != null && storeKey != null) { - try { - ctx.put(storeKey, sig.clone()); - } catch (RuntimeException ignore) { // NOPMD - } - } - if (cb != null) { - try { - cb.accept(sig.clone()); - } catch (RuntimeException ignore) { // NOPMD - } - } - - byte[] outBytes = switch (out) { - case SIG_RAW -> sig; - case SIG_HEX -> java.util.HexFormat.of().formatHex(sig).getBytes(StandardCharsets.UTF_8); - case SIG_BASE64 -> Base64.getEncoder().encode(sig); - default -> throw new IllegalStateException("Unexpected output: " + out); - }; - return new ByteArrayInputStream(outBytes); - } - } - - /** - * Pass-through verifier that forwards bytes unchanged while verifying a - * signature at EOF. - * - *

      - * {@code VerifyPassthrough} attaches to an upstream {@link DataContent}, - * returns an {@link InputStream} that yields the original payload, and performs - * signature verification as the stream is consumed. The expected signature is - * provided directly or fetched from a {@link CtxInterface}. When the stream - * reaches EOF, the configured verification strategy determines whether a - * mismatch raises an error or is handled differently (for example, by flagging - * in a context if the supplied strategy implements that). - *

      - * - *

      Behavior

      - *
        - *
      • Expected signature is taken from the {@code expected} field or from - * {@code ctx.get(expectedKey)}.
      • - *
      • The returned stream must be fully drained or closed to finalize - * verification.
      • - *
      - */ - private final class VerifyPassthrough implements PlainContent { - private final CryptoAlgorithm alg; - private final PublicKey key; - private final byte[] expected; - private final Key expectedKey; - private final CtxInterface ctx; - private final SignatureVerificationStrategy strategy; - private volatile DataContent upstream; // NOPMD - - /** - * Creates a pass-through verifier that reads from upstream and verifies at EOF. - * - * @param alg algorithm used to obtain a {@link SignatureContext}; must - * not be {@code null} - * @param key public key used for verification; must not be {@code null} - * @param expected expected signature bytes (defensively copied), or - * {@code null} to fetch from {@code ctx} - * @param expectedKey key in {@code ctx} under which the expected signature may - * be stored; may be {@code null} - * @param ctx optional context used to fetch the expected signature; may - * be {@code null} - * @param strategy verification approach; if {@code null}, a default - * throw-on-mismatch strategy is used - * @throws NullPointerException if {@code alg} or {@code key} is {@code null} - */ - private VerifyPassthrough(CryptoAlgorithm alg, PublicKey key, byte[] expected, Key expectedKey, - CtxInterface ctx, SignatureVerificationStrategy strategy) { - this.alg = Objects.requireNonNull(alg); - this.key = Objects.requireNonNull(key); - this.expected = (expected == null ? null : expected.clone()); - this.expectedKey = expectedKey; - this.ctx = ctx; - this.strategy = strategy; - } - - /** - * Sets the upstream content that will be passed through and verified. - * - * @param input upstream data source; must not be {@code null} - * @throws NullPointerException if {@code input} is {@code null} - */ - @Override - public void setInput(DataContent input) { - this.upstream = Objects.requireNonNull(input); - } - - /** - * Returns a stream that forwards upstream bytes and performs signature - * verification. - * - *

      - * The method creates a {@link SignatureContext}, configures the expected - * signature and verification policy, and wraps the upstream stream. The - * returned stream must be consumed to EOF (or closed) to finalize verification - * and, if configured, to store/emit the result. - *

      - * - * @return an input stream that yields the original bytes while verifying at EOF - * @throws IOException if the verify context cannot be initialized or - * the wrapping fails - * @throws IllegalStateException if no expected signature is available via - * constructor or context - * @throws NullPointerException if {@link #setInput(DataContent)} was not - * invoked prior to this call - */ - @Override - public InputStream getStream() throws IOException { - Objects.requireNonNull(upstream, "verify: missing input"); - - byte[] exp = expected; - if (exp == null && ctx != null && expectedKey != null) { - exp = ctx.get(expectedKey); - } - if (exp == null) { - throw new IllegalStateException("VERIFY requires expectedSignature (or ctx+key)"); - } - - final SignatureContext sc; // NOPMD - try { - sc = newVerifyContext(alg, key); - } catch (GeneralSecurityException e) { - throw new IOException("Failed to init verify context", e); - } - - sc.setExpectedTag(exp); - if (strategy == null) { - sc.setVerificationApproach(sc.getVerificationCore().getThrowOnMismatch()); - } else { - sc.setVerificationApproach(strategy); - } - - try { - return sc.wrap(upstream.getStream()); - } catch (IOException initFail) { - try { - sc.close(); - } catch (RuntimeException ignore) { // NOPMD - } - throw initFail; - } - } - } - - /** - * Streaming content wrapper that verifies a digital signature and emits the - * result as a boolean value. - * - *

      - * {@code VerifyEmit} consumes an upstream {@link DataContent}, initializes a - * {@link SignatureContext} with the supplied public key and expected signature, - * and verifies the signature while streaming the input. The verification - * outcome ({@code true} or {@code false}) is then returned as a one-shot - * {@link java.io.InputStream} containing the UTF-8 encoded string - * {@code "true"} or {@code "false"}. - *

      - * - *

      Notes

      - *
        - *
      • The expected signature may be provided directly or looked up from a - * {@link CtxInterface} via {@code expectedKey}.
      • - *
      • Outcome computation follows the configured verification strategy; I/O or - * verification failures result in {@code "false"}.
      • - *
      - */ - private final class VerifyEmit implements PlainContent { - private final CryptoAlgorithm alg; - private final PublicKey key; - private final byte[] expected; - private final Key expectedKey; - private final CtxInterface ctx; - private final SignatureVerificationStrategy strategy; - private volatile DataContent upstream; // NOPMD - - /** - * Creates a streaming verifier that consumes upstream data and emits a boolean - * result. - * - * @param alg algorithm used to create a {@link SignatureContext}; must - * not be {@code null} - * @param key public key used for verification; must not be {@code null} - * @param expected expected signature bytes; may be {@code null} when - * {@code ctx} and {@code expectedKey} are provided - * @param expectedKey context key from which to read the expected signature when - * {@code expected} is {@code null}; may be {@code null} - * @param ctx optional context used to read the expected signature; may - * be {@code null} - * @param strategy verification approach; if {@code null}, a default - * throw-on-mismatch strategy is used - * @throws NullPointerException if {@code alg} or {@code key} is {@code null} - */ - private VerifyEmit(CryptoAlgorithm alg, PublicKey key, byte[] expected, Key expectedKey, - CtxInterface ctx, SignatureVerificationStrategy strategy) { - this.alg = Objects.requireNonNull(alg); - this.key = Objects.requireNonNull(key); - this.expected = (expected == null ? null : expected.clone()); - this.expectedKey = expectedKey; - this.ctx = ctx; - this.strategy = strategy; - } - - /** - * Sets the upstream content that will be consumed and verified. - * - *

      - * This method must be called exactly once before {@link #getStream()}. The - * provided {@link DataContent} is stored and later used to obtain the readable - * stream that is fed into the verification context. - *

      - * - * @param input upstream data source; must not be {@code null} - * @throws NullPointerException if {@code input} is {@code null} - */ - @Override - public void setInput(DataContent input) { - this.upstream = Objects.requireNonNull(input); - } - - /** - * Returns a one-shot stream that yields the UTF-8 text {@code "true"} or - * {@code "false"} after verifying the upstream content against the expected - * signature. - * - *

      - * On entry, this method initializes a {@link SignatureContext}, configures the - * expected tag and a strict verification policy, and then drains the upstream - * stream. Any I/O failures or verification mismatches result in the boolean - * outcome {@code false}; successful verification results in {@code true}. The - * outcome is optionally stored into {@code ctx} under {@code storeOk} and - * passed to the callback {@code cb}. - *

      - * - *

      - * Resource management is handled via try-with-resources: the verification - * context is always closed. If closing the context fails, the method reports - * {@code false} rather than propagating the close exception, allowing callers - * to reliably consume the result. - *

      - * - *

      Usage

      {@code
      -         * verifyEmit.setInput(data);
      -         * try (InputStream result = verifyEmit.getStream()) {
      -         *     boolean ok = Boolean.parseBoolean(new String(result.readAllBytes(), StandardCharsets.UTF_8));
      -         *     // use ok
      -         * }
      -         * }
      - * - * @return a new input stream that produces {@code "true"} or {@code "false"} in - * UTF-8 - * @throws IOException if the verify context cannot be initialized or - * the upstream stream cannot be obtained - * @throws IllegalStateException if no expected signature is available from the - * constructor arguments or the context - * @throws NullPointerException if {@link #setInput(DataContent)} was not - * called before invocation - */ - @Override - public InputStream getStream() throws IOException { - Objects.requireNonNull(upstream, "verify: missing input"); - - byte[] exp = expected; - if (exp == null && ctx != null && expectedKey != null) { - exp = ctx.get(expectedKey); - } - if (exp == null) { - throw new IllegalStateException("VERIFY requires expectedSignature (or ctx+key)"); - } - - final SignatureContext sc; - try { - sc = newVerifyContext(alg, key); - } catch (GeneralSecurityException e) { - throw new IOException("Failed to init verify context", e); - } - - sc.setExpectedTag(exp); - if (strategy == null) { - sc.setVerificationApproach(sc.getVerificationCore().getThrowOnMismatch()); - } else { - sc.setVerificationApproach(strategy); - } - - try (sc) { // closes sc; if it throws, that exception propagates - try (InputStream in = sc.wrap(upstream.getStream())) { - in.transferTo(OutputStream.nullOutputStream()); - return new ByteArrayInputStream("true".getBytes(StandardCharsets.UTF_8)); - } catch (IOException fail) { - // wrap/read/transfer/close(in) failures land here and are swallowed -> ok = - // false - return new ByteArrayInputStream("false".getBytes(StandardCharsets.UTF_8)); - } - } - } - } -} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/AesDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/AesDataContentBuilder.java index a0e7e01..ae57445 100644 --- a/lib/src/main/java/zeroecho/sdk/builders/alg/AesDataContentBuilder.java +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/AesDataContentBuilder.java @@ -33,6 +33,8 @@ ******************************************************************************/ package zeroecho.sdk.builders.alg; +import zeroecho.sdk.ZeroEchoSession; + import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; @@ -44,7 +46,6 @@ import javax.crypto.SecretKey; import conflux.Ctx; import conflux.CtxInterface; import zeroecho.core.ConfluxKeys; -import zeroecho.core.CryptoAlgorithm; import zeroecho.core.CryptoAlgorithms; import zeroecho.core.KeyUsage; import zeroecho.core.SymmetricHeaderCodec; @@ -54,7 +55,6 @@ import zeroecho.core.alg.aes.AesKeyImportSpec; import zeroecho.core.alg.aes.AesSpec; import zeroecho.core.context.EncryptionContext; import zeroecho.core.spi.ContextAware; -import zeroecho.core.spi.SymmetricKeyBuilder; import zeroecho.sdk.builders.core.DataContentBuilder; import zeroecho.sdk.content.api.DataContent; import zeroecho.sdk.content.api.EncryptedContent; @@ -106,6 +106,8 @@ import zeroecho.sdk.content.api.PlainContent; * @since 1.0 */ public final class AesDataContentBuilder implements DataContentBuilder { + private static final String ALGORITHM_ID = "AES"; + private final ZeroEchoSession session; private SecretKey secretKey; private AesKeyGenSpec genSpec; private AesKeyImportSpec importSpec; @@ -128,11 +130,12 @@ public final class AesDataContentBuilder implements DataContentBuilder 0) { if (ctx == null) { ctx = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime()); } - ctx.put(ConfluxKeys.iv("AES"), iv); + ctx.put(ConfluxKeys.iv(ALGORITHM_ID), iv); } return encrypt ? new EncryptContent(key, aesSpec, ctx) : new DecryptContent(key, aesSpec, ctx); @@ -387,18 +390,14 @@ public final class AesDataContentBuilder implements DataContentBuilder b = algo.symmetricKeyBuilder(AesKeyGenSpec.class); - generatedKey = b.generateSecret(genSpec); + generatedKey = session.keyBuilders().symmetric().generate(ALGORITHM_ID, genSpec); return generatedKey; } if (importSpec != null) { - SymmetricKeyBuilder b = algo.symmetricKeyBuilder(AesKeyImportSpec.class); - return b.importSecret(importSpec); + return session.keyBuilders().symmetric().importKey(ALGORITHM_ID, importSpec); } - SymmetricKeyBuilder b = algo.symmetricKeyBuilder(AesKeyGenSpec.class); - generatedKey = b.generateSecret(AesKeyGenSpec.aes256()); + generatedKey = session.keyBuilders().symmetric().generate(ALGORITHM_ID, AesKeyGenSpec.aes256()); return generatedKey; } catch (GeneralSecurityException e) { throw new IllegalStateException("AES key construction failed", e); @@ -446,7 +445,7 @@ public final class AesDataContentBuilder implements DataContentBuilder */ - private static final class EncryptContent implements EncryptedContent { + private final class EncryptContent implements EncryptedContent { private final SecretKey key; private final AesSpec spec; private final CtxInterface ctx; // may be null @@ -523,7 +522,7 @@ public final class AesDataContentBuilder implements DataContentBuilderThread-safety Instances are not thread-safe and are intended for * single-use pipelines. */ - private static final class DecryptContent implements PlainContent { + private final class DecryptContent implements PlainContent { private final SecretKey key; private final AesSpec spec; private final CtxInterface ctx; // may be null @@ -609,7 +608,7 @@ public final class AesDataContentBuilder implements DataContentBuilder * The method creates a decryption * {@link zeroecho.core.context.EncryptionContext} via - * {@link CryptoAlgorithms#create(String, KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}, + * {@link zeroecho.sdk.ZeroEchoSession#createContext(String, KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}, * forwards the optional {@link CtxInterface} when the context is * {@code ContextAware}, and returns the stream produced by {@code attach}. The * returned stream is independent of the temporary context and remains usable @@ -628,7 +627,7 @@ public final class AesDataContentBuilder implements DataContentBuilder { + private final ZeroEchoSession session; private SecretKey secretKey; private ChaChaKeyGenSpec genSpec; @@ -174,11 +174,12 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder b = algo.symmetricKeyBuilder(ChaChaKeyGenSpec.class); - return b.generateSecret(genSpec); + return session.keyBuilders().symmetric().generate(algId, genSpec); } if (importSpec != null) { - SymmetricKeyBuilder b = algo.symmetricKeyBuilder(ChaChaKeyImportSpec.class); - return b.importSecret(importSpec); + return session.keyBuilders().symmetric().importKey(algId, importSpec); } - SymmetricKeyBuilder b = algo.symmetricKeyBuilder(ChaChaKeyGenSpec.class); - return b.generateSecret(ChaChaKeyGenSpec.chacha256()); + return session.keyBuilders().symmetric().generate(algId, ChaChaKeyGenSpec.chacha256()); } catch (GeneralSecurityException e) { throw new IllegalStateException("ChaCha key construction failed for " + algId, e); } @@ -567,12 +564,13 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder * The actual cipher work is delegated to an * {@link zeroecho.core.context.EncryptionContext} created through - * {@link zeroecho.core.CryptoAlgorithms#create(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}. + * {@link zeroecho.sdk.ZeroEchoSession#createContext(String, + * zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}. * If the created context implements {@code ContextAware}, the configured * context is injected before the stream is attached. *

      */ - private static final class EncryptContent implements EncryptedContent { + private final class EncryptContent implements EncryptedContent { private final String algId; private final SecretKey key; private final S spec; @@ -615,7 +613,7 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder * The actual cipher work is delegated to an * {@link zeroecho.core.context.EncryptionContext} created through - * {@link zeroecho.core.CryptoAlgorithms#create(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}. + * {@link zeroecho.sdk.ZeroEchoSession#createContext(String, + * zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}. * If the created context implements {@code ContextAware}, the configured * context is injected before the stream is attached. *

      */ - private static final class DecryptContent implements PlainContent { + private final class DecryptContent implements PlainContent { private final String algId; private final SecretKey key; private final S spec; @@ -686,7 +685,7 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder { + private final ZeroEchoSession session; /** * OutputMode selects how the digest-computing pipeline presents its result to * callers. @@ -184,7 +186,8 @@ public final class DigestDataContentBuilder implements DataContentBuilder callback; // optional private int bufferSize = 8192; // internal I/O buffer for tail-stripper - private DigestDataContentBuilder() { + private DigestDataContentBuilder(ZeroEchoSession session) { + this.session = Objects.requireNonNull(session, "session must not be null"); } /** @@ -200,8 +203,8 @@ public final class DigestDataContentBuilder implements DataContentBuilder storeKey; @@ -444,7 +447,8 @@ public final class DigestDataContentBuilder implements DataContentBuilder storeKey; @@ -514,7 +518,8 @@ public final class DigestDataContentBuilder implements DataContentBuilderOverview This builder specializes - * {@link AbstractStreamingSignatureDataBuilder} for ECDSA and lets callers - * choose a named curve before constructing a signing or verification pipeline. - * The actual signing and verification work is performed by - * {@link SignatureContext} instances created via JCA-backed factories. - * - *

      Typical usage

      {@code
      - * // Sign while passing the original bytes through:
      - * PlainContent signed = EcdsaDataContentBuilder.builder()
      - *     .withCurveP256()
      - *     .sign()
      - *     .withPrivateKey(privateKey)
      - *     .passThrough()
      - *     .build(true);
      - *
      - * // Emit a detached Base64 signature:
      - * PlainContent sigOut = EcdsaDataContentBuilder.builder()
      - *     .withCurve(EcdsaCurveSpec.P384)
      - *     .sign()
      - *     .withPrivateKey(privateKey)
      - *     .emitBase64Signature()
      - *     .build(true);
      - *
      - * // Verify while passing the original bytes through:
      - * PlainContent verified = EcdsaDataContentBuilder.builder()
      - *     .withCurveP256()
      - *     .verify()
      - *     .withPublicKey(publicKey)
      - *     .expectedSignature(rawSig)
      - *     .passThrough()
      - *     .build(true);
      - * }
      - * - *

      Curve selection

      If no curve is selected explicitly, {@code P256} is - * used. Convenience methods are provided for P-256, P-384, and P-512 - * (implementation-specific name used by {@link EcdsaCurveSpec}). - * - *

      Thread-safety

      Instances are mutable and not thread-safe. Configure - * and use each builder instance from a single thread. - * - * @see AbstractStreamingSignatureDataBuilder - * @see EcdsaCurveSpec - * @see EcdsaPublicKeySpec - * @see EcdsaPrivateKeySpec - * @see SignatureContext - * @see GenericJcaSignatureContext - */ -public final class EcdsaDataContentBuilder - extends AbstractStreamingSignatureDataBuilder { - - private final static EcdsaCurveSpec DEFAULT = EcdsaCurveSpec.P256; - - private EcdsaCurveSpec selected = DEFAULT; - - /** - * Creates a new builder instance with the default curve selection. - * - *

      Example

      {@code
      -     * EcdsaDataContentBuilder b = EcdsaDataContentBuilder.builder();
      -     * }
      - * - * @return a new {@code EcdsaDataContentBuilder} - */ - public static EcdsaDataContentBuilder builder() { - return new EcdsaDataContentBuilder(); - } - - /** - * Selects the curve to be used for key generation and signature processing. - * - * @param spec the ECDSA curve specification; must not be null - * @return {@code this} builder for chaining - * @throws NullPointerException if {@code spec} is null - */ - public EcdsaDataContentBuilder withCurve(final EcdsaCurveSpec spec) { - Objects.requireNonNull(spec, "EcdsaCurveSpec cannot be null"); - this.selected = spec; - return this; - } - - /** - * Selects the P-256 curve. - * - * @return {@code this} builder for chaining - */ - public EcdsaDataContentBuilder withCurveP256() { - this.selected = EcdsaCurveSpec.P256; - return this; - } - - /** - * Selects the P-384 curve. - * - * @return {@code this} builder for chaining - */ - public EcdsaDataContentBuilder withCurveP384() { - this.selected = EcdsaCurveSpec.P384; - return this; - } - - /** - * Selects the P-512 curve as defined by {@link EcdsaCurveSpec}. - * - * @return {@code this} builder for chaining - */ - public EcdsaDataContentBuilder withCurveP512() { - this.selected = EcdsaCurveSpec.P512; - return this; - } - - /** - * Returns the algorithm name used to resolve an implementation from - * {@link CryptoAlgorithms}. - * - * @return the string {@code "ECDSA"} - */ - @Override - protected String algorithmName() { - return "ECDSA"; - } - - private EcdsaCurveSpec activeSpec() { - return selected; - } - - /** - * Creates a signing {@link SignatureContext} for the active curve using the - * provided algorithm and key. - * - *

      - * The returned context is configured with a JCA signature factory derived from - * the curve's {@link EcdsaCurveSpec#jcaFactory()} and a fixed-length resolver - * based on {@link EcdsaCurveSpec#signFixedLength()}. - *

      - * - * @param alg the resolved crypto algorithm - * @param key the private key to use for signing - * @return a new signature context in sign mode - * @throws GeneralSecurityException if the context cannot be created - */ - @Override - protected SignatureContext newSignContext(final CryptoAlgorithm alg, final PrivateKey key) - throws GeneralSecurityException { - EcdsaCurveSpec s = activeSpec(); - return new GenericJcaSignatureContext(alg, key, GenericJcaSignatureContext.jcaFactory(s.jcaFactory(), null), - GenericJcaSignatureContext.SignLengthResolver.fixed(s.signFixedLength())); - } - - /** - * Creates a verification {@link SignatureContext} for the active curve using - * the provided algorithm and key. - * - *

      - * The returned context is configured with a JCA signature factory derived from - * the curve's {@link EcdsaCurveSpec#jcaFactory()} and a fixed-length resolver - * based on {@link EcdsaCurveSpec#signFixedLength()}. - *

      - * - * @param alg the resolved crypto algorithm - * @param key the public key to use for verification - * @return a new signature context in verify mode - * @throws GeneralSecurityException if the context cannot be created - */ - @Override - protected SignatureContext newVerifyContext(final CryptoAlgorithm alg, final PublicKey key) - throws GeneralSecurityException { - EcdsaCurveSpec s = activeSpec(); - return new GenericJcaSignatureContext(alg, key, GenericJcaSignatureContext.jcaFactory(s.jcaFactory(), null), - GenericJcaSignatureContext.VerifyLengthResolver.fixed(s.signFixedLength())); - } - - /** - * Returns the class object of the key generation specification used by this - * builder. - * - * @return {@code EcdsaCurveSpec.class} - */ - @Override - protected Class keyGenSpecClass() { - return EcdsaCurveSpec.class; - } - - /** - * Returns the class object of the public key import specification used by this - * builder. - * - * @return {@code EcdsaPublicKeySpec.class} - */ - @Override - protected Class publicKeySpecClass() { - return EcdsaPublicKeySpec.class; - } - - /** - * Returns the class object of the private key import specification used by this - * builder. - * - * @return {@code EcdsaPrivateKeySpec.class} - */ - @Override - protected Class privateKeySpecClass() { - return EcdsaPrivateKeySpec.class; - } - - /** - * Supplies the default key generation specification for this builder. - * - * @return a supplier that returns {@link EcdsaCurveSpec#P256} - */ - @Override - protected Supplier defaultKeyGenSpecSupplier() { - return () -> DEFAULT; - } - - /** - * Returns the currently selected key generation specification, or null to - * indicate that the default should be used. - * - * @return the active {@link EcdsaCurveSpec} or {@code null} - */ - @Override - protected EcdsaCurveSpec currentKeyGenSpecOrNull() { - return selected; - } - - /** - * Creates a public key import specification from X.509-encoded bytes. - * - * @param x509 the SubjectPublicKeyInfo bytes - * @param provider an optional provider name hint, ignored by this - * implementation - * @return a new {@link EcdsaPublicKeySpec} wrapping the provided bytes - */ - @Override - protected EcdsaPublicKeySpec makePublicKeySpec(final byte[] x509, final String provider) { - return new EcdsaPublicKeySpec(x509); - } - - /** - * Creates a private key import specification from PKCS#8-encoded bytes. - * - * @param pkcs8 the PrivateKeyInfo bytes - * @param provider an optional provider name hint, ignored by this - * implementation - * @return a new {@link EcdsaPrivateKeySpec} wrapping the provided bytes - */ - @Override - protected EcdsaPrivateKeySpec makePrivateKeySpec(final byte[] pkcs8, final String provider) { - return new EcdsaPrivateKeySpec(pkcs8); - } - - /** - * Returns the default provider hint to use when importing keys if none is - * explicitly set. - * - * @return {@code null} to indicate no preference - */ - @Override - protected String defaultProviderHint() { - return null; - } -} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/Ed25519DataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/Ed25519DataContentBuilder.java deleted file mode 100644 index ea5b302..0000000 --- a/lib/src/main/java/zeroecho/sdk/builders/alg/Ed25519DataContentBuilder.java +++ /dev/null @@ -1,253 +0,0 @@ -/******************************************************************************* - * Copyright (C) 2026, Leo Galambos - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software must - * display the following acknowledgement: - * This product includes software developed by the Egothor project. - * - * 4. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ******************************************************************************/ -package zeroecho.sdk.builders.alg; - -import java.security.GeneralSecurityException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.util.function.Supplier; - -import zeroecho.core.CryptoAlgorithm; -import zeroecho.core.alg.ed25519.Ed25519KeyGenSpec; -import zeroecho.core.alg.ed25519.Ed25519PrivateKeySpec; -import zeroecho.core.alg.ed25519.Ed25519PublicKeySpec; -import zeroecho.core.alg.ed25519.Ed25519SignatureContext; -import zeroecho.core.context.SignatureContext; - -/** - * Ed25519DataContentBuilder builds streaming Ed25519 signature pipelines that - * sign or verify as an InputStream is consumed. - * - *

      Overview

      This builder specializes - * {@link AbstractStreamingSignatureDataBuilder} for the Ed25519 algorithm. It - * constructs {@link zeroecho.sdk.content.api.PlainContent} that either passes - * the original bytes through while computing or checking a detached signature, - * or emits the signature or verification result directly, depending on the - * configuration provided by the fluent API defined in the superclass. - * - *

      Typical usage

      {@code
      - * // Sign while passing the original bytes through:
      - * PlainContent signed = Ed25519DataContentBuilder.builder()
      - *     .sign()
      - *     .withPrivateKey(privateKey)
      - *     .passThrough()
      - *     .build(true);
      - *
      - * // Emit a Base64 detached signature:
      - * PlainContent sigOut = Ed25519DataContentBuilder.builder()
      - *     .sign()
      - *     .withPrivateKey(privateKey)
      - *     .emitBase64Signature()
      - *     .build(true);
      - *
      - * // Verify against an expected signature while passing data through:
      - * PlainContent verified = Ed25519DataContentBuilder.builder()
      - *     .verify()
      - *     .withPublicKey(publicKey)
      - *     .expectedSignature(rawSig)
      - *     .passThrough()
      - *     .build(true);
      - * }
      - * - *

      Key handling

      Ed25519 has no tunable parameters for key generation in - * this builder. A default generation supplier is provided, and imports from - * X.509 (public) and PKCS#8 (private) encodings are supported. - * - *

      Thread-safety

      Instances are mutable and not thread-safe. Configure - * and use each builder instance from a single thread. - * - * @see AbstractStreamingSignatureDataBuilder - * @see zeroecho.core.CryptoAlgorithms - * @see zeroecho.core.context.SignatureContext - */ -public final class Ed25519DataContentBuilder - extends AbstractStreamingSignatureDataBuilder { - - private static final Supplier DEFAULT_GEN = Ed25519KeyGenSpec::defaultSpec; - - /** - * Creates a new builder instance for constructing Ed25519 streaming signature - * pipelines. - * - *

      Example

      {@code
      -     * Ed25519DataContentBuilder b = Ed25519DataContentBuilder.builder();
      -     * }
      - * - * @return a new {@code Ed25519DataContentBuilder} - */ - public static Ed25519DataContentBuilder builder() { - return new Ed25519DataContentBuilder(); - } - - /** - * Returns the canonical algorithm name used to resolve an implementation from - * {@link zeroecho.core.CryptoAlgorithms}. - * - * @return the string {@code "Ed25519"} - */ - @Override - protected String algorithmName() { - return "Ed25519"; - } - - /** - * Creates a signing {@link SignatureContext} for Ed25519 using the provided - * algorithm instance and private key. - * - * @param alg the resolved algorithm instance used to create the context - * @param key the private key for signing - * @return a new {@link SignatureContext} configured for Ed25519 signing - * @throws GeneralSecurityException if the context cannot be created for the - * given key or algorithm - */ - @Override - protected SignatureContext newSignContext(final CryptoAlgorithm alg, final PrivateKey key) - throws GeneralSecurityException { - return new Ed25519SignatureContext(alg, key); - } - - /** - * Creates a verification {@link SignatureContext} for Ed25519 using the - * provided algorithm instance and public key. - * - * @param alg the resolved algorithm instance used to create the context - * @param key the public key for verification - * @return a new {@link SignatureContext} configured for Ed25519 verification - * @throws GeneralSecurityException if the context cannot be created for the - * given key or algorithm - */ - @Override - protected SignatureContext newVerifyContext(final CryptoAlgorithm alg, final PublicKey key) - throws GeneralSecurityException { - return new Ed25519SignatureContext(alg, key); - } - - /** - * Returns the key generation specification class used by this builder. - * - * @return {@code Ed25519KeyGenSpec.class} - */ - @Override - protected Class keyGenSpecClass() { - return Ed25519KeyGenSpec.class; - } - - /** - * Returns the public key import specification class used by this builder. - * - * @return {@code Ed25519PublicKeySpec.class} - */ - @Override - protected Class publicKeySpecClass() { - return Ed25519PublicKeySpec.class; - } - - /** - * Returns the private key import specification class used by this builder. - * - * @return {@code Ed25519PrivateKeySpec.class} - */ - @Override - protected Class privateKeySpecClass() { - return Ed25519PrivateKeySpec.class; - } - - /** - * Supplies a default key generation specification for Ed25519. - * - * @return a supplier returning {@link Ed25519KeyGenSpec#defaultSpec()} - */ - @Override - protected Supplier defaultKeyGenSpecSupplier() { - return DEFAULT_GEN; - } - - /** - * Returns the currently configured key generation specification or null to - * indicate the default should be used. - * - *

      - * Ed25519 has no tunables in this builder, so this method returns {@code null}. - *

      - * - * @return {@code null} - */ - @Override - protected Ed25519KeyGenSpec currentKeyGenSpecOrNull() { - return null; // no tunables for Ed25519 - } - - /** - * Builds a public key import specification from X.509-encoded - * SubjectPublicKeyInfo bytes. - * - * @param x509 the X.509 public key bytes - * @param ignoredProvider an optional provider hint, ignored by this - * implementation - * @return a new {@link Ed25519PublicKeySpec} wrapping the provided bytes - */ - @Override - protected Ed25519PublicKeySpec makePublicKeySpec(final byte[] x509, final String ignoredProvider) { - return new Ed25519PublicKeySpec(x509); - } - - /** - * Builds a private key import specification from PKCS#8-encoded PrivateKeyInfo - * bytes. - * - * @param pkcs8 the PKCS#8 private key bytes - * @param ignoredProvider an optional provider hint, ignored by this - * implementation - * @return a new {@link Ed25519PrivateKeySpec} wrapping the provided bytes - */ - @Override - protected Ed25519PrivateKeySpec makePrivateKeySpec(final byte[] pkcs8, final String ignoredProvider) { - return new Ed25519PrivateKeySpec(pkcs8); - } - - /** - * Returns the default provider hint used for key imports when none is - * explicitly supplied. - * - *

      - * This builder relies on the JDK default provider for Ed25519. - *

      - * - * @return {@code null} to indicate no provider preference - */ - @Override - protected String defaultProviderHint() { - return null; // use JDK default provider - } -} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/Ed448DataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/Ed448DataContentBuilder.java deleted file mode 100644 index f8e1a5a..0000000 --- a/lib/src/main/java/zeroecho/sdk/builders/alg/Ed448DataContentBuilder.java +++ /dev/null @@ -1,274 +0,0 @@ -/******************************************************************************* - * Copyright (C) 2026, Leo Galambos - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software must - * display the following acknowledgement: - * This product includes software developed by the Egothor project. - * - * 4. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ******************************************************************************/ -package zeroecho.sdk.builders.alg; - -import java.security.GeneralSecurityException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.util.function.Supplier; - -import zeroecho.core.CryptoAlgorithm; -import zeroecho.core.alg.ed448.Ed448KeyGenSpec; -import zeroecho.core.alg.ed448.Ed448PrivateKeySpec; -import zeroecho.core.alg.ed448.Ed448PublicKeySpec; -import zeroecho.core.alg.ed448.Ed448SignatureContext; -import zeroecho.core.context.SignatureContext; - -/** - * Builder for constructing streaming Ed448 signature and verification - * {@link zeroecho.sdk.content.api.DataContent} pipelines. - * - *

      - * {@code Ed448DataContentBuilder} is a thin adapter around - * {@link AbstractStreamingSignatureDataBuilder} that binds the generic - * streaming signature framework to the Ed448 algorithm. It supports both - * signing and verification flows, with key material provided via - * {@link Ed448KeyGenSpec}, {@link Ed448PublicKeySpec}, and - * {@link Ed448PrivateKeySpec}. - *

      - * - *

      Usage example

      {@code
      - * // Create a builder for signing
      - * Ed448DataContentBuilder builder = Ed448DataContentBuilder.builder()
      - *     .sign()
      - *     .generateKeyPair()
      - *     .emitBase64Signature();
      - *
      - * // Build a content pipeline
      - * PlainContent content = builder.build(true);
      - * content.setInput(originalContent);
      - *
      - * try (InputStream in = content.getStream()) {
      - *     byte[] signature = in.readAllBytes();
      - * }
      - * }
      - * - *

      Design notes

      - *
        - *
      • Uses {@link Ed448SignatureContext} for both signing and - * verification.
      • - *
      • Key generation is parameterized by {@link Ed448KeyGenSpec}, but Ed448 - * exposes no runtime tunables; the default is always used.
      • - *
      • Public/private key imports are supported via X.509 and PKCS#8 wrappers - * respectively.
      • - *
      • No provider hints are necessary; the JDK default is assumed.
      • - *
      - * - * @see Ed448SignatureContext - * @see Ed448KeyGenSpec - * @see Ed448PublicKeySpec - * @see Ed448PrivateKeySpec - * @since 1.0 - */ -public final class Ed448DataContentBuilder - extends AbstractStreamingSignatureDataBuilder { - - private static final Supplier DEFAULT_GEN = Ed448KeyGenSpec::defaultSpec; - - /** - * Creates a new builder instance for constructing Ed448 streaming signature - * pipelines. - * - *

      Example

      {@code
      -     * Ed448DataContentBuilder b = Ed448DataContentBuilder.builder();
      -     * }
      - * - * @return a new {@code Ed448DataContentBuilder} - */ - public static Ed448DataContentBuilder builder() { - return new Ed448DataContentBuilder(); - } - - /** - * Returns the canonical algorithm name used to resolve an implementation from - * {@link zeroecho.core.CryptoAlgorithms}. - * - * @return the string {@code "Ed448"} - */ - @Override - protected String algorithmName() { - return "Ed448"; - } - - /** - * Creates a signing {@link SignatureContext} for Ed448 using the provided - * algorithm instance and private key. - * - *

      - * The returned context is configured to accept streaming updates and to produce - * a detached signature tag at end-of-stream. - *

      - * - *

      Example

      {@code
      -     * SignatureContext sc = newSignContext(alg, privateKey);
      -     * try (InputStream in = sc.wrap(upstream)) {
      -     *     in.transferTo(OutputStream.nullOutputStream());
      -     * }
      -     * }
      - * - * @param alg the resolved algorithm instance - * @param key the private key used for signing - * @return a new signature context in sign mode - * @throws GeneralSecurityException if the context cannot be created for the - * given key or algorithm - */ - @Override - protected SignatureContext newSignContext(CryptoAlgorithm alg, PrivateKey key) throws GeneralSecurityException { - return new Ed448SignatureContext(alg, key); - } - - /** - * Creates a verification {@link SignatureContext} for Ed448 using the provided - * algorithm instance and public key. - * - *

      - * The returned context is configured to accept streaming updates and to - * validate the expected detached signature tag at end-of-stream. - *

      - * - * @param alg the resolved algorithm instance - * @param key the public key used for verification - * @return a new signature context in verify mode - * @throws GeneralSecurityException if the context cannot be created for the - * given key or algorithm - */ - @Override - protected SignatureContext newVerifyContext(CryptoAlgorithm alg, PublicKey key) throws GeneralSecurityException { - return new Ed448SignatureContext(alg, key); - } - - /** - * Returns the key generation specification class used by this builder. - * - * @return {@code Ed448KeyGenSpec.class} - */ - @Override - protected Class keyGenSpecClass() { - return Ed448KeyGenSpec.class; - } - - /** - * Returns the public key import specification class used by this builder. - * - * @return {@code Ed448PublicKeySpec.class} - */ - @Override - protected Class publicKeySpecClass() { - return Ed448PublicKeySpec.class; - } - - /** - * Returns the private key import specification class used by this builder. - * - * @return {@code Ed448PrivateKeySpec.class} - */ - @Override - protected Class privateKeySpecClass() { - return Ed448PrivateKeySpec.class; - } - - /** - * Supplies the default key generation specification for Ed448. - * - * @return a supplier returning {@link Ed448KeyGenSpec#defaultSpec()} - */ - @Override - protected Supplier defaultKeyGenSpecSupplier() { - return DEFAULT_GEN; - } - - /** - * Returns the currently configured key generation specification or null to - * indicate that the default should be used. - * - *

      - * Ed448 has no tunables in this builder, so this method returns {@code null}. - *

      - * - * @return {@code null} - */ - @Override - protected Ed448KeyGenSpec currentKeyGenSpecOrNull() { - return null; // no options - } - - /** - * Builds a public key import specification from X.509-encoded - * SubjectPublicKeyInfo bytes. - * - *

      Example

      {@code
      -     * Ed448PublicKeySpec spec = makePublicKeySpec(spkiBytes, null);
      -     * }
      - * - * @param x509 X.509 public key bytes (SubjectPublicKeyInfo) - * @param ignoredProvider optional provider hint, ignored by this implementation - * @return a new {@link Ed448PublicKeySpec} wrapping the provided bytes - */ - @Override - protected Ed448PublicKeySpec makePublicKeySpec(byte[] x509, String ignoredProvider) { - return new Ed448PublicKeySpec(x509); - } - - /** - * Builds a private key import specification from PKCS#8-encoded PrivateKeyInfo - * bytes. - * - *

      Example

      {@code
      -     * Ed448PrivateKeySpec spec = makePrivateKeySpec(pkcs8Bytes, null);
      -     * }
      - * - * @param pkcs8 PKCS#8 private key bytes (PrivateKeyInfo) - * @param ignoredProvider optional provider hint, ignored by this implementation - * @return a new {@link Ed448PrivateKeySpec} wrapping the provided bytes - */ - @Override - protected Ed448PrivateKeySpec makePrivateKeySpec(byte[] pkcs8, String ignoredProvider) { - return new Ed448PrivateKeySpec(pkcs8); - } - - /** - * Returns the default provider hint used for key imports when none is - * explicitly supplied. - * - *

      - * This builder relies on the JDK default provider for Ed448. - *

      - * - * @return {@code null} to indicate no provider preference - */ - @Override - protected String defaultProviderHint() { - return null; // JDK default - } -} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/ElgamalEncDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/ElgamalEncDataContentBuilder.java index 32b097e..d7571dc 100644 --- a/lib/src/main/java/zeroecho/sdk/builders/alg/ElgamalEncDataContentBuilder.java +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/ElgamalEncDataContentBuilder.java @@ -33,6 +33,8 @@ ******************************************************************************/ package zeroecho.sdk.builders.alg; +import zeroecho.sdk.ZeroEchoSession; + import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; @@ -41,8 +43,6 @@ import java.security.PrivateKey; import java.security.PublicKey; import java.util.Objects; -import zeroecho.core.CryptoAlgorithm; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.KeyUsage; import zeroecho.core.alg.elgamal.ElgamalEncSpec; import zeroecho.core.alg.elgamal.ElgamalKeyGenSpec; @@ -50,7 +50,6 @@ import zeroecho.core.alg.elgamal.ElgamalParamSpec; import zeroecho.core.alg.elgamal.ElgamalPrivateKeySpec; import zeroecho.core.alg.elgamal.ElgamalPublicKeySpec; import zeroecho.core.context.EncryptionContext; -import zeroecho.core.spi.AsymmetricKeyBuilder; import zeroecho.sdk.builders.core.DataContentBuilder; import zeroecho.sdk.content.api.DataContent; import zeroecho.sdk.content.api.EncryptedContent; @@ -109,6 +108,8 @@ import zeroecho.sdk.content.api.PlainContent; * @see ElgamalParamSpec */ public final class ElgamalEncDataContentBuilder implements DataContentBuilder { + private static final String ALGORITHM_ID = "ElGamal"; + private final ZeroEchoSession session; private PublicKey publicKey; private PrivateKey privateKey; @@ -123,7 +124,8 @@ public final class ElgamalEncDataContentBuilder implements DataContentBuilder b = alg.asymmetricKeyBuilder(ElgamalKeyGenSpec.class); - KeyPair kp = b.generateKeyPair(keyGen); + KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair(ALGORITHM_ID, keyGen); this.publicKey = kp.getPublic(); this.privateKey = kp.getPrivate(); } else if (genKeyPairPredef) { - AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(ElgamalParamSpec.class); - KeyPair kp = b.generateKeyPair(paramSpec); + KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair(ALGORITHM_ID, paramSpec); this.publicKey = kp.getPublic(); this.privateKey = kp.getPrivate(); } if (importPrivatePkcs8 != null) { - AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(ElgamalPrivateKeySpec.class); - this.privateKey = b.importPrivate(new ElgamalPrivateKeySpec(importPrivatePkcs8)); + this.privateKey = session.keyBuilders().asymmetric().importPrivate(ALGORITHM_ID, + new ElgamalPrivateKeySpec(importPrivatePkcs8)); } if (importPublicX509 != null) { - AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(ElgamalPublicKeySpec.class); - this.publicKey = b.importPublic(new ElgamalPublicKeySpec(importPublicX509)); + this.publicKey = session.keyBuilders().asymmetric().importPublic(ALGORITHM_ID, + new ElgamalPublicKeySpec(importPublicX509)); } } @@ -363,7 +362,8 @@ public final class ElgamalEncDataContentBuilder implements DataContentBuilder @@ -371,7 +371,7 @@ public final class ElgamalEncDataContentBuilder implements DataContentBuilderThread-safety Instances are not thread-safe and must be used from a * single thread. */ - private static final class Encrypt implements EncryptedContent { + private final class Encrypt implements EncryptedContent { private final PublicKey key; private final ElgamalEncSpec spec; private DataContent upstream; @@ -412,7 +412,7 @@ public final class ElgamalEncDataContentBuilder implements DataContentBuilder @@ -435,7 +436,7 @@ public final class ElgamalEncDataContentBuilder implements DataContentBuilderThread-safety Instances are not thread-safe and must be used from a * single thread. */ - private static final class Decrypt implements PlainContent { + private final class Decrypt implements PlainContent { private final PrivateKey key; private final ElgamalEncSpec spec; private DataContent upstream; @@ -476,7 +477,7 @@ public final class ElgamalEncDataContentBuilder implements DataContentBuilder { + private static final String ALGORITHM_ID = "HMAC"; + private final ZeroEchoSession session; /** * Mode selects whether the pipeline computes an HMAC tag or verifies one. * @@ -233,7 +234,8 @@ public final class HmacDataContentBuilder implements DataContentBuilder b = algo.symmetricKeyBuilder(HmacKeyGenSpec.class); - return b.generateSecret(new HmacKeyGenSpec(mac, genKeyBits)); // builder honors macName + return session.keyBuilders().symmetric().generate(ALGORITHM_ID, + new HmacKeyGenSpec(mac, genKeyBits)); } if (importRaw != null || importHex != null || importBase64 != null) { - SymmetricKeyBuilder b = algo.symmetricKeyBuilder(HmacKeyImportSpec.class); HmacKeyImportSpec ispec; if (importRaw != null) { ispec = HmacKeyImportSpec.fromRaw(mac, importRaw); @@ -635,11 +635,10 @@ public final class HmacDataContentBuilder implements DataContentBuilder b = algo.symmetricKeyBuilder(HmacKeyGenSpec.class); - return b.generateSecret(HmacKeyGenSpec.sha256(256)); + return session.keyBuilders().symmetric().generate(ALGORITHM_ID, HmacKeyGenSpec.sha256(256)); } catch (GeneralSecurityException e) { throw new IllegalStateException("HMAC key construction failed", e); } @@ -661,7 +660,7 @@ public final class HmacDataContentBuilder implements DataContentBuilder { + private final ZeroEchoSession session; // ---------- KEM configuration ---------- private String kemId; private PublicKey recipientPublic; @@ -126,7 +129,8 @@ public final class KemDataContentBuilder implements DataContentBuilderThread-safety Instances are not thread-safe. Each instance must be * configured and used from a single thread. */ - private static final class EncryptContent implements EncryptedContent { + private final class EncryptContent implements EncryptedContent { private final String kemId; private final PublicKey recipientPublic; private final boolean useHkdf; @@ -416,35 +420,35 @@ public final class KemDataContentBuilder implements DataContentBuilder maxKemCtLen) { + throw new IOException("KEM ciphertext length invalid: " + ciphertext.length); + } - final KemContext kem = CryptoAlgorithms.create(kemId, KeyUsage.ENCAPSULATE, recipientPublic, // NOPMD - VoidSpec.INSTANCE); - final KemResult kr = kem.encapsulate(); - final byte[] ct = Objects.requireNonNull(kr.ciphertext(), "kem ciphertext"); - final byte[] shared = Objects.requireNonNull(kr.sharedSecret(), "kem sharedSecret"); - - if (ct.length <= 0 || ct.length > maxKemCtLen) { - throw new IOException("KEM ciphertext length invalid: " + ct.length); + DataContent symmetric = buildSymmetric(shared, true); + symmetric.setInput(upstream); + InputStream kemPrefix = writeLenPrefixedAsStream(ciphertext); + InputStream payloadStream = symmetric.getStream(); + return new SequenceInputStream(kemPrefix, payloadStream); + } finally { + if (shared != null) { + Arrays.fill(shared, (byte) 0); + } } + } - // Configure symmetric algorithm - DataContent symmetric; - + private DataContent buildSymmetric(byte[] shared, boolean encrypt) throws IOException { if (aesBuilder != null) { - // aes - final SecretKey payloadKey = deriveKey(shared, "AES", derivedKeyBytes, useHkdf, hkdfInfo); - symmetric = aesBuilder.withKey(payloadKey).build(true); - } else { - // chacha20 - final SecretKey payloadKey = deriveKey(shared, "ChaCha20", derivedKeyBytes, useHkdf, hkdfInfo); - symmetric = chachaBuilder.withKey(payloadKey).build(true); + SecretKey payloadKey = deriveKey(shared, "AES", derivedKeyBytes, useHkdf, hkdfInfo); + return aesBuilder.withKey(payloadKey).build(encrypt); } - - symmetric.setInput(upstream); - - final InputStream kemPrefix = writeLenPrefixedAsStream(ct); - final InputStream payloadStream = symmetric.getStream(); - return new SequenceInputStream(kemPrefix, payloadStream); + SecretKey payloadKey = deriveKey(shared, "ChaCha20", derivedKeyBytes, useHkdf, hkdfInfo); + return chachaBuilder.withKey(payloadKey).build(encrypt); } } @@ -466,7 +470,7 @@ public final class KemDataContentBuilder implements DataContentBuilderThread-safety Instances are not thread-safe. Each instance must be * configured and used from a single thread. */ - private static final class DecryptContent implements DataContent { // PlainContent would also be fine + private final class DecryptContent implements DataContent { // PlainContent would also be fine private final String kemId; private final PrivateKey recipientPrivate; private final boolean useHkdf; @@ -523,33 +527,44 @@ public final class KemDataContentBuilder implements DataContentBuilder - * This builder integrates ML-DSA with the reusable streaming pipeline provided - * by {@link AbstractStreamingSignatureDataBuilder}. It supports signing or - * verifying data while it flows through an {@link java.io.InputStream}, as well - * as emitting detached signature artifacts in raw, hex or Base64 encodings. - *

      - * - *

      - * Key material may be provided directly, imported (X.509 / PKCS#8), or - * generated on demand using an algorithm-specific {@link MldsaKeyGenSpec}. - *

      - * - * @since 1.0 - */ -public final class MldsaDataContentBuilder - extends AbstractStreamingSignatureDataBuilder { - - private MldsaKeyGenSpec keyGenSpec; - - /** - * Creates a new ML-DSA streaming builder instance. - * - * @return new builder - */ - public static MldsaDataContentBuilder builder() { - return new MldsaDataContentBuilder(); - } - - /** - * Sets a non-default key generation specification used when - * {@link #generateKeyPair()} is requested. - * - * @param spec key generation spec; must not be {@code null} - * @return {@code this} for chaining - * @throws NullPointerException if {@code spec} is {@code null} - */ - public MldsaDataContentBuilder withKeyGenSpec(MldsaKeyGenSpec spec) { - this.keyGenSpec = Objects.requireNonNull(spec); - return this; - } - - @Override - protected String algorithmName() { - return "ML-DSA"; - } - - @Override - protected SignatureContext newSignContext(CryptoAlgorithm alg, PrivateKey key) throws GeneralSecurityException { - return new MldsaSignatureContext(alg, key); - } - - @Override - protected SignatureContext newVerifyContext(CryptoAlgorithm alg, PublicKey key) throws GeneralSecurityException { - return new MldsaSignatureContext(alg, key); - } - - @Override - protected Class keyGenSpecClass() { - return MldsaKeyGenSpec.class; - } - - @Override - protected Class publicKeySpecClass() { - return MldsaPublicKeySpec.class; - } - - @Override - protected Class privateKeySpecClass() { - return MldsaPrivateKeySpec.class; - } - - @Override - protected Supplier defaultKeyGenSpecSupplier() { - return MldsaKeyGenSpec::defaultSpec; - } - - @Override - protected MldsaKeyGenSpec currentKeyGenSpecOrNull() { - return keyGenSpec; - } - - @Override - protected MldsaPublicKeySpec makePublicKeySpec(byte[] x509, String providerHint) { - return (providerHint == null) ? new MldsaPublicKeySpec(x509) : new MldsaPublicKeySpec(x509, providerHint); - } - - @Override - protected MldsaPrivateKeySpec makePrivateKeySpec(byte[] pkcs8, String providerHint) { - return (providerHint == null) ? new MldsaPrivateKeySpec(pkcs8) : new MldsaPrivateKeySpec(pkcs8, providerHint); - } - - @Override - protected String defaultProviderHint() { - return "BC"; - } - - // Optional: covariant fluent overrides for better chaining ergonomics. - - @Override - public MldsaDataContentBuilder sign() { - super.sign(); - return this; - } - - @Override - public MldsaDataContentBuilder verify() { - super.verify(); - return this; - } - - @Override - public MldsaDataContentBuilder passThrough() { - super.passThrough(); - return this; - } - - @Override - public MldsaDataContentBuilder emitRawSignature() { - super.emitRawSignature(); - return this; - } - - @Override - public MldsaDataContentBuilder emitHexSignature() { - super.emitHexSignature(); - return this; - } - - @Override - public MldsaDataContentBuilder emitBase64Signature() { - super.emitBase64Signature(); - return this; - } - - @Override - public MldsaDataContentBuilder emitVerificationBoolean() { - super.emitVerificationBoolean(); - return this; - } - - @Override - public MldsaDataContentBuilder bufferSize(int bytes) { - super.bufferSize(bytes); - return this; - } - - @Override - public MldsaDataContentBuilder withPrivateKey(PrivateKey k) { - super.withPrivateKey(k); - return this; - } - - @Override - public MldsaDataContentBuilder withPublicKey(PublicKey k) { - super.withPublicKey(k); - return this; - } - - @Override - public MldsaDataContentBuilder generateKeyPair() { - super.generateKeyPair(); - return this; - } - - @Override - public MldsaDataContentBuilder importPrivatePkcs8(byte[] pkcs8) { - super.importPrivatePkcs8(pkcs8); - return this; - } - - @Override - public MldsaDataContentBuilder importPrivatePkcs8(byte[] pkcs8, String providerName) { - super.importPrivatePkcs8(pkcs8, providerName); - return this; - } - - @Override - public MldsaDataContentBuilder importPublicX509(byte[] x509) { - super.importPublicX509(x509); - return this; - } - - @Override - public MldsaDataContentBuilder importPublicX509(byte[] x509, String providerName) { - super.importPublicX509(x509, providerName); - return this; - } - - @Override - public MldsaDataContentBuilder expectedSignature(byte[] raw) { - super.expectedSignature(raw); - return this; - } - - @Override - public MldsaDataContentBuilder expectedSignatureHex(String hex) { - super.expectedSignatureHex(hex); - return this; - } - - @Override - public MldsaDataContentBuilder expectedSignatureBase64(String b64) { - super.expectedSignatureBase64(b64); - return this; - } -} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/RsaEncDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/RsaEncDataContentBuilder.java index 843c734..3a29f81 100644 --- a/lib/src/main/java/zeroecho/sdk/builders/alg/RsaEncDataContentBuilder.java +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/RsaEncDataContentBuilder.java @@ -33,6 +33,8 @@ ******************************************************************************/ package zeroecho.sdk.builders.alg; +import zeroecho.sdk.ZeroEchoSession; + import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; @@ -41,15 +43,12 @@ import java.security.PrivateKey; import java.security.PublicKey; import java.util.Objects; -import zeroecho.core.CryptoAlgorithm; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.KeyUsage; import zeroecho.core.alg.rsa.RsaEncSpec; import zeroecho.core.alg.rsa.RsaKeyGenSpec; import zeroecho.core.alg.rsa.RsaPrivateKeySpec; import zeroecho.core.alg.rsa.RsaPublicKeySpec; import zeroecho.core.context.EncryptionContext; -import zeroecho.core.spi.AsymmetricKeyBuilder; import zeroecho.sdk.builders.core.DataContentBuilder; import zeroecho.sdk.content.api.DataContent; import zeroecho.sdk.content.api.EncryptedContent; @@ -102,6 +101,7 @@ import zeroecho.sdk.content.api.PlainContent; * @see RsaKeyGenSpec */ public final class RsaEncDataContentBuilder implements DataContentBuilder { + private final ZeroEchoSession session; private PublicKey publicKey; private PrivateKey privateKey; @@ -114,7 +114,8 @@ public final class RsaEncDataContentBuilder implements DataContentBuilder b = alg.asymmetricKeyBuilder(RsaKeyGenSpec.class); - KeyPair kp = b.generateKeyPair(keyGen); + KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("RSA", keyGen); this.publicKey = kp.getPublic(); this.privateKey = kp.getPrivate(); } if (importPrivatePkcs8 != null) { - AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(RsaPrivateKeySpec.class); - this.privateKey = b.importPrivate(new RsaPrivateKeySpec(importPrivatePkcs8)); + this.privateKey = session.keyBuilders().asymmetric().importPrivate("RSA", + new RsaPrivateKeySpec(importPrivatePkcs8)); } if (importPublicX509 != null) { - AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(RsaPublicKeySpec.class); - this.publicKey = b.importPublic(new RsaPublicKeySpec(importPublicX509)); + this.publicKey = session.keyBuilders().asymmetric().importPublic("RSA", + new RsaPublicKeySpec(importPublicX509)); } } @@ -347,7 +346,8 @@ public final class RsaEncDataContentBuilder implements DataContentBuilder @@ -355,7 +355,7 @@ public final class RsaEncDataContentBuilder implements DataContentBuilderThread-safety Instances are not thread-safe and must be used from a * single thread. */ - private static final class Encrypt implements EncryptedContent { + private final class Encrypt implements EncryptedContent { private final PublicKey key; private final RsaEncSpec spec; private DataContent upstream; @@ -394,7 +394,7 @@ public final class RsaEncDataContentBuilder implements DataContentBuilder @@ -416,7 +417,7 @@ public final class RsaEncDataContentBuilder implements DataContentBuilderThread-safety Instances are not thread-safe and must be used from a * single thread. */ - private static final class Decrypt implements PlainContent { + private final class Decrypt implements PlainContent { private final PrivateKey key; private final RsaEncSpec spec; private DataContent upstream; @@ -455,7 +456,7 @@ public final class RsaEncDataContentBuilder implements DataContentBuilder { + private static final String ALGORITHM_ID = "RSA"; + private final ZeroEchoSession session; /** * Mode selects whether the builder signs or verifies. */ @@ -189,7 +190,8 @@ public final class RsaSigDataContentBuilder implements DataContentBuilder onSignature; private Consumer onVerified; - private RsaSigDataContentBuilder() { + private RsaSigDataContentBuilder(ZeroEchoSession session) { + this.session = Objects.requireNonNull(session, "session must not be null"); } /** @@ -201,8 +203,8 @@ public final class RsaSigDataContentBuilder implements DataContentBuilder b = alg.asymmetricKeyBuilder(RsaKeyGenSpec.class); - KeyPair kp = b.generateKeyPair(keyGen); + KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair(ALGORITHM_ID, keyGen); this.privateKey = kp.getPrivate(); this.publicKey = kp.getPublic(); } if (importPrivatePkcs8 != null) { - AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(RsaPrivateKeySpec.class); - this.privateKey = b.importPrivate(new RsaPrivateKeySpec(importPrivatePkcs8)); + this.privateKey = session.keyBuilders().asymmetric().importPrivate(ALGORITHM_ID, + new RsaPrivateKeySpec(importPrivatePkcs8)); } if (importPublicX509 != null) { - AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(RsaPublicKeySpec.class); - this.publicKey = b.importPublic(new RsaPublicKeySpec(importPublicX509)); + this.publicKey = session.keyBuilders().asymmetric().importPublic(ALGORITHM_ID, + new RsaPublicKeySpec(importPublicX509)); } } /** * SIGN + pass-through: return body only; capture signature from trailer. */ - private static final class SignPass implements PlainContent { + private final class SignPass implements PlainContent { private final PrivateKey key; private final RsaSigSpec spec; private final Consumer cb; @@ -534,7 +534,7 @@ public final class RsaSigDataContentBuilder implements DataContentBuilder - * This builder integrates SLH-DSA with the reusable streaming pipeline provided - * by {@link AbstractStreamingSignatureDataBuilder}. It supports signing or - * verifying data while it flows through an {@link java.io.InputStream}, as well - * as emitting detached signature artifacts in raw, hex or Base64 encodings. - *

      - * - *

      - * Key material may be provided directly, imported (X.509 / PKCS#8), or - * generated on demand using an algorithm-specific {@link SlhDsaKeyGenSpec}. - *

      - * - * @since 1.0 - */ -public final class SlhDsaDataContentBuilder - extends AbstractStreamingSignatureDataBuilder { - - private SlhDsaKeyGenSpec keyGenSpec; - - /** - * Creates a new SLH-DSA streaming builder instance. - * - * @return new builder - */ - public static SlhDsaDataContentBuilder builder() { - return new SlhDsaDataContentBuilder(); - } - - /** - * Sets a non-default key generation specification used when - * {@link #generateKeyPair()} is requested. - * - * @param spec key generation spec; must not be {@code null} - * @return {@code this} for chaining - * @throws NullPointerException if {@code spec} is {@code null} - */ - public SlhDsaDataContentBuilder withKeyGenSpec(SlhDsaKeyGenSpec spec) { - this.keyGenSpec = Objects.requireNonNull(spec); - return this; - } - - @Override - protected String algorithmName() { - return "SLH-DSA"; - } - - @Override - protected SignatureContext newSignContext(CryptoAlgorithm alg, PrivateKey key) throws GeneralSecurityException { - return new SlhDsaSignatureContext(alg, key); - } - - @Override - protected SignatureContext newVerifyContext(CryptoAlgorithm alg, PublicKey key) throws GeneralSecurityException { - return new SlhDsaSignatureContext(alg, key); - } - - @Override - protected Class keyGenSpecClass() { - return SlhDsaKeyGenSpec.class; - } - - @Override - protected Class publicKeySpecClass() { - return SlhDsaPublicKeySpec.class; - } - - @Override - protected Class privateKeySpecClass() { - return SlhDsaPrivateKeySpec.class; - } - - @Override - protected Supplier defaultKeyGenSpecSupplier() { - return SlhDsaKeyGenSpec::defaultSpec; - } - - @Override - protected SlhDsaKeyGenSpec currentKeyGenSpecOrNull() { - return keyGenSpec; - } - - @Override - protected SlhDsaPublicKeySpec makePublicKeySpec(byte[] x509, String providerHint) { - return (providerHint == null) ? new SlhDsaPublicKeySpec(x509) : new SlhDsaPublicKeySpec(x509, providerHint); - } - - @Override - protected SlhDsaPrivateKeySpec makePrivateKeySpec(byte[] pkcs8, String providerHint) { - return (providerHint == null) ? new SlhDsaPrivateKeySpec(pkcs8) : new SlhDsaPrivateKeySpec(pkcs8, providerHint); - } - - @Override - protected String defaultProviderHint() { - return "BC"; - } - - // Optional: covariant fluent overrides for better chaining ergonomics. - - @Override - public SlhDsaDataContentBuilder sign() { - super.sign(); - return this; - } - - @Override - public SlhDsaDataContentBuilder verify() { - super.verify(); - return this; - } - - @Override - public SlhDsaDataContentBuilder passThrough() { - super.passThrough(); - return this; - } - - @Override - public SlhDsaDataContentBuilder emitRawSignature() { - super.emitRawSignature(); - return this; - } - - @Override - public SlhDsaDataContentBuilder emitHexSignature() { - super.emitHexSignature(); - return this; - } - - @Override - public SlhDsaDataContentBuilder emitBase64Signature() { - super.emitBase64Signature(); - return this; - } - - @Override - public SlhDsaDataContentBuilder emitVerificationBoolean() { - super.emitVerificationBoolean(); - return this; - } - - @Override - public SlhDsaDataContentBuilder bufferSize(int bytes) { - super.bufferSize(bytes); - return this; - } - - @Override - public SlhDsaDataContentBuilder withPrivateKey(PrivateKey k) { - super.withPrivateKey(k); - return this; - } - - @Override - public SlhDsaDataContentBuilder withPublicKey(PublicKey k) { - super.withPublicKey(k); - return this; - } - - @Override - public SlhDsaDataContentBuilder generateKeyPair() { - super.generateKeyPair(); - return this; - } - - @Override - public SlhDsaDataContentBuilder importPrivatePkcs8(byte[] pkcs8) { - super.importPrivatePkcs8(pkcs8); - return this; - } - - @Override - public SlhDsaDataContentBuilder importPrivatePkcs8(byte[] pkcs8, String providerName) { - super.importPrivatePkcs8(pkcs8, providerName); - return this; - } - - @Override - public SlhDsaDataContentBuilder importPublicX509(byte[] x509) { - super.importPublicX509(x509); - return this; - } - - @Override - public SlhDsaDataContentBuilder importPublicX509(byte[] x509, String providerName) { - super.importPublicX509(x509, providerName); - return this; - } - - @Override - public SlhDsaDataContentBuilder expectedSignature(byte[] raw) { - super.expectedSignature(raw); - return this; - } - - @Override - public SlhDsaDataContentBuilder expectedSignatureHex(String hex) { - super.expectedSignatureHex(hex); - return this; - } - - @Override - public SlhDsaDataContentBuilder expectedSignatureBase64(String b64) { - super.expectedSignatureBase64(b64); - return this; - } -} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/SphincsPlusDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/SphincsPlusDataContentBuilder.java deleted file mode 100644 index 8584b10..0000000 --- a/lib/src/main/java/zeroecho/sdk/builders/alg/SphincsPlusDataContentBuilder.java +++ /dev/null @@ -1,300 +0,0 @@ -/******************************************************************************* - * Copyright (C) 2026, Leo Galambos - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software must - * display the following acknowledgement: - * This product includes software developed by the Egothor project. - * - * 4. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ******************************************************************************/ -package zeroecho.sdk.builders.alg; - -import java.security.GeneralSecurityException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.util.Objects; -import java.util.function.Supplier; - -import zeroecho.core.CryptoAlgorithm; -import zeroecho.core.CryptoAlgorithms; -import zeroecho.core.alg.sphincsplus.SphincsPlusKeyGenSpec; -import zeroecho.core.alg.sphincsplus.SphincsPlusPrivateKeySpec; -import zeroecho.core.alg.sphincsplus.SphincsPlusPublicKeySpec; -import zeroecho.core.alg.sphincsplus.SphincsPlusSignatureContext; -import zeroecho.core.context.SignatureContext; - -/** - * Builder for streaming signature operations using the SPHINCS+ post-quantum - * signature scheme. - * - *

      - * This builder follows the {@link AbstractStreamingSignatureDataBuilder} - * pattern and provides configuration options to generate keys, import keys, and - * perform signing or verification in a streaming pipeline. SPHINCS+ is a - * hash-based, stateless signature scheme designed for post-quantum security, - * standardized by NIST PQC Round 3. - *

      - * - *

      Key management

      - *
        - *
      • Keys can be generated using a custom {@link SphincsPlusKeyGenSpec} - * supplied via {@link #withKeyGenSpec(SphincsPlusKeyGenSpec)}, or with the - * algorithm's default specification.
      • - *
      • Public and private keys may be imported from encoded formats - * ({@code X.509} and {@code PKCS#8}, respectively).
      • - *
      • A provider hint may optionally be given. If not provided, the builder - * will prefer the provider specified in {@link SphincsPlusKeyGenSpec}, or fall - * back to {@link #defaultProviderHint()}.
      • - *
      - * - *

      Contexts

      - *
        - *
      • {@link #newSignContext(CryptoAlgorithm, java.security.PrivateKey)} - * produces a {@link SphincsPlusSignatureContext} in sign mode.
      • - *
      • {@link #newVerifyContext(CryptoAlgorithm, java.security.PublicKey)} - * produces a {@link SphincsPlusSignatureContext} in verify mode.
      • - *
      • Contexts are bound to the {@link CryptoAlgorithm} instance returned by - * {@link CryptoAlgorithms#require(String)} with id {@code "SPHINCS+"}.
      • - *
      - * - *

      Thread-safety

      Instances of this builder are not thread-safe. Each - * builder should be configured and built within a single thread or with - * external synchronization. - * - *

      Example

      {@code
      - * // Signing with SPHINCS+
      - * SphincsPlusDataContentBuilder builder = SphincsPlusDataContentBuilder.builder()
      - *      .sign()
      - *      .withKeyGenSpec(SphincsPlusKeyGenSpec.sphincsSha256128s());
      - *
      - * PlainContent content = builder.build(true);
      - * content.setInput(new FileContent("document.txt"));
      - * try (InputStream in = content.getStream()) {
      - *     // Consume input to trigger signing
      - * }
      - * }
      - * - * @see SphincsPlusKeyGenSpec - * @see SphincsPlusPublicKeySpec - * @see SphincsPlusPrivateKeySpec - * @see SphincsPlusSignatureContext - * @since 1.0 - */ -public final class SphincsPlusDataContentBuilder extends - AbstractStreamingSignatureDataBuilder { - /** - * Optional custom key generation specification to be used when - * {@link #generateKeyPair()} is selected in the superclass API. - */ - private SphincsPlusKeyGenSpec keyGenSpec; // optional custom spec - - /** - * Creates a new builder for constructing SPHINCS+ streaming signature - * pipelines. - * - *

      Example

      {@code
      -     * SphincsPlusDataContentBuilder b = SphincsPlusDataContentBuilder.builder();
      -     * }
      - * - * @return a new {@code SphincsPlusDataContentBuilder} instance - */ - public static SphincsPlusDataContentBuilder builder() { - return new SphincsPlusDataContentBuilder(); - } - - /** - * Sets a custom key generation specification to be used for SPHINCS+ key pair - * generation. - * - *

      - * If no specification is provided, the builder falls back to the supplier - * returned by {@link #defaultKeyGenSpecSupplier()}. - *

      - * - * @param spec the SPHINCS+ key generation specification; must not be null - * @return {@code this} builder for chaining - * @throws NullPointerException if {@code spec} is null - */ - public SphincsPlusDataContentBuilder withKeyGenSpec(SphincsPlusKeyGenSpec spec) { - this.keyGenSpec = Objects.requireNonNull(spec); - return this; - } - - /** - * Returns the algorithm identifier used to resolve the implementation from - * {@link zeroecho.core.CryptoAlgorithms}. - * - * @return the string {@code "SPHINCS+"} - */ - @Override - protected String algorithmName() { - return "SPHINCS+"; - } - - /** - * Creates a signing {@link zeroecho.core.context.SignatureContext} for SPHINCS+ - * using the provided algorithm instance and private key. - * - * @param alg the resolved algorithm instance used to create the context - * @param key the private key for signing - * @return a new {@link SphincsPlusSignatureContext} configured for signing - * @throws GeneralSecurityException if the context cannot be created for the - * given key or algorithm - */ - @Override - protected SignatureContext newSignContext(CryptoAlgorithm alg, PrivateKey key) throws GeneralSecurityException { - return new SphincsPlusSignatureContext(alg, key); - } - - /** - * Creates a verification {@link zeroecho.core.context.SignatureContext} for - * SPHINCS+ using the provided algorithm instance and public key. - * - * @param alg the resolved algorithm instance used to create the context - * @param key the public key for verification - * @return a new {@link SphincsPlusSignatureContext} configured for verification - * @throws GeneralSecurityException if the context cannot be created for the - * given key or algorithm - */ - @Override - protected SignatureContext newVerifyContext(CryptoAlgorithm alg, PublicKey key) throws GeneralSecurityException { - return new SphincsPlusSignatureContext(alg, key); - } - - /** - * Returns the key generation specification class used by this builder. - * - * @return {@code SphincsPlusKeyGenSpec.class} - */ - @Override - protected Class keyGenSpecClass() { - return SphincsPlusKeyGenSpec.class; - } - - /** - * Returns the public key import specification class used by this builder. - * - * @return {@code SphincsPlusPublicKeySpec.class} - */ - @Override - protected Class publicKeySpecClass() { - return SphincsPlusPublicKeySpec.class; - } - - /** - * Returns the private key import specification class used by this builder. - * - * @return {@code SphincsPlusPrivateKeySpec.class} - */ - @Override - protected Class privateKeySpecClass() { - return SphincsPlusPrivateKeySpec.class; - } - - /** - * Supplies a default key generation specification for SPHINCS+. - * - * @return a supplier returning {@link SphincsPlusKeyGenSpec#defaultSpec()} - */ - @Override - protected Supplier defaultKeyGenSpecSupplier() { - return SphincsPlusKeyGenSpec::defaultSpec; - } - - /** - * Returns the currently configured key generation specification or null to - * indicate that the default should be used. - * - * @return the active {@link SphincsPlusKeyGenSpec} or {@code null} if none has - * been set - */ - @Override - protected SphincsPlusKeyGenSpec currentKeyGenSpecOrNull() { - return keyGenSpec; - } - - /** - * Builds a public key import specification from X.509-encoded - * SubjectPublicKeyInfo bytes and an optional provider hint. - * - *

      - * Provider selection follows this precedence: - *

        - *
      1. Use {@code providerHint} if non-null.
      2. - *
      3. Else, if a custom key generation spec is present, use - * {@link SphincsPlusKeyGenSpec#providerName()}.
      4. - *
      5. Else, use {@link #defaultProviderHint()}.
      6. - *
      - * - * @param x509 the X.509 public key bytes - * @param providerHint an optional provider name hint; may be null - * @return a new {@link SphincsPlusPublicKeySpec} wrapping the provided bytes - * and provider choice - */ - @Override - protected SphincsPlusPublicKeySpec makePublicKeySpec(byte[] x509, String providerHint) { - String p = providerHint != null ? providerHint - : keyGenSpec != null ? keyGenSpec.providerName() : defaultProviderHint(); - return new SphincsPlusPublicKeySpec(x509, p); - } - - /** - * Builds a private key import specification from PKCS#8-encoded PrivateKeyInfo - * bytes and an optional provider hint. - * - *

      - * Provider selection follows this precedence: - *

        - *
      1. Use {@code providerHint} if non-null.
      2. - *
      3. Else, if a custom key generation spec is present, use - * {@link SphincsPlusKeyGenSpec#providerName()}.
      4. - *
      5. Else, use {@link #defaultProviderHint()}.
      6. - *
      - * - * @param pkcs8 the PKCS#8 private key bytes - * @param providerHint an optional provider name hint; may be null - * @return a new {@link SphincsPlusPrivateKeySpec} wrapping the provided bytes - * and provider choice - */ - @Override - protected SphincsPlusPrivateKeySpec makePrivateKeySpec(byte[] pkcs8, String providerHint) { - String p = providerHint != null ? providerHint - : keyGenSpec != null ? keyGenSpec.providerName() : defaultProviderHint(); - return new SphincsPlusPrivateKeySpec(pkcs8, p); - } - - /** - * Returns the default provider hint to use when importing SPHINCS+ keys if none - * is explicitly specified. - * - * @return the default provider hint string, typically {@code "BCPQC"} - */ - @Override - protected String defaultProviderHint() { - return "BCPQC"; - } -} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/package-info.java b/lib/src/main/java/zeroecho/sdk/builders/alg/package-info.java index d36dbf8..0f9c507 100644 --- a/lib/src/main/java/zeroecho/sdk/builders/alg/package-info.java +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/package-info.java @@ -65,11 +65,9 @@ * supported, emit textual encodings. *
    141. {@link RsaEncDataContentBuilder} and {@link ElgamalEncDataContentBuilder} * - wrap asymmetric encryption.
    142. - *
    143. {@link RsaSigDataContentBuilder}, {@link EcdsaDataContentBuilder}, - * {@link Ed25519DataContentBuilder}, {@link Ed448DataContentBuilder}, - * {@link SphincsPlusDataContentBuilder}, {@link SlhDsaDataContentBuilder}, and - * {@link MldsaDataContentBuilder} - perform streaming signatures and - * verification.
    144. + *
    145. {@link RsaSigDataContentBuilder} - performs streaming RSA signatures and + * verification; other signature algorithms use the session-bound trailer + * builder in {@code zeroecho.sdk.builders}.
    146. *
    147. {@link KemDataContentBuilder} - implement KEM-first envelopes and inject * the derived key into a chosen symmetric payload builder.
    148. * @@ -87,8 +85,7 @@ * *

      Signatures and MACs

      *

      - * Signature builders share common mechanics via - * {@link AbstractStreamingSignatureDataBuilder}. They support: + * Signature and MAC builders support: *

      *
        *
      • Pass-through: output the message body while a trailing signature diff --git a/lib/src/main/java/zeroecho/sdk/builders/package-info.java b/lib/src/main/java/zeroecho/sdk/builders/package-info.java index 07589a6..f94557d 100644 --- a/lib/src/main/java/zeroecho/sdk/builders/package-info.java +++ b/lib/src/main/java/zeroecho/sdk/builders/package-info.java @@ -68,13 +68,9 @@ *
      • Asymmetric encryption: * {@link zeroecho.sdk.builders.alg.RsaEncDataContentBuilder}, * {@link zeroecho.sdk.builders.alg.ElgamalEncDataContentBuilder}.
      • - *
      • Signatures: {@link zeroecho.sdk.builders.alg.RsaSigDataContentBuilder}, - * {@link zeroecho.sdk.builders.alg.EcdsaDataContentBuilder}, - * {@link zeroecho.sdk.builders.alg.Ed25519DataContentBuilder}, - * {@link zeroecho.sdk.builders.alg.Ed448DataContentBuilder}, - * {@link zeroecho.sdk.builders.alg.SphincsPlusDataContentBuilder}, - * {@link zeroecho.sdk.builders.alg.MldsaDataContentBuilder}, - * {@link zeroecho.sdk.builders.alg.SlhDsaDataContentBuilder}.
      • + *
      • RSA signatures: + * {@link zeroecho.sdk.builders.alg.RsaSigDataContentBuilder}; generic signature + * trailers use {@link zeroecho.sdk.builders.SignatureTrailerDataContentBuilder}.
      • *
      • MAC and digest: {@link zeroecho.sdk.builders.alg.HmacDataContentBuilder}, * {@link zeroecho.sdk.builders.alg.DigestDataContentBuilder}.
      • *
      • KEM envelopes: {@link zeroecho.sdk.builders.alg.KemDataContentBuilder} diff --git a/lib/src/main/java/zeroecho/sdk/content/builtin/SecretPassword.java b/lib/src/main/java/zeroecho/sdk/content/builtin/SecretPassword.java index fc35785..5dacde3 100644 --- a/lib/src/main/java/zeroecho/sdk/content/builtin/SecretPassword.java +++ b/lib/src/main/java/zeroecho/sdk/content/builtin/SecretPassword.java @@ -33,48 +33,218 @@ ******************************************************************************/ package zeroecho.sdk.content.builtin; -import conflux.Ctx; -import conflux.Key; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; + import zeroecho.sdk.content.api.SecretContent; +import zeroecho.sdk.content.api.DataContent; import zeroecho.sdk.util.Password; /** - * A {@link SecretContent} implementation that encapsulates a passwordKey - * string. This class extends {@link PlainString} and enforces immutability of - * the passwordKey after construction. + * Destroyable password content backed exclusively by an owned character array. + * *

        - * Passwords can be generated randomly or provided explicitly. Once set, - * attempts to change the passwordKey via parameters will cause an exception. + * Textual diagnostics are always redacted. Callers obtain explicit copies + * through {@link #chars()} or {@link #toBytes()} and remain responsible for + * wiping those copies. + *

        + * *

        - * This class supports applying parameters from a map and collecting its state - * back into a map, using the key {@link #PASSWORD}. - * + * Lifecycle operations are synchronized. Destruction is idempotent, wipes the + * complete owned character array, and prevents subsequent access to password + * material. Streams returned by {@link #getStream()} wipe their private UTF-8 + * buffer when closed. + *

        + * * @author Leo Galambos */ -public class SecretPassword extends PlainString implements SecretContent { - private final Key PASSWORD = Key.of("secret.password", String.class); +public final class SecretPassword implements SecretContent, Destroyable { + private static final String REDACTED = "[REDACTED]"; + private final char[] password; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** - * Constructs a {@code SecretPassword} with a randomly generated printable - * passwordKey of the specified length. + * Constructs a password with randomly generated printable characters. * * @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); + this(Password.generatePrintablePasswordChars(length), true); } /** - * Constructs a {@code SecretPassword} wrapping the specified passwordKey - * string. The passwordKey may be {@code null}. + * Constructs a password from a caller-owned character array. The supplied + * array is cloned and remains owned by the caller. * - * @param password the passwordKey string, or {@code null} + * @param password password characters; must not be {@code null} + * @throws NullPointerException if {@code password} is {@code null} */ - public SecretPassword(final String password) { - super(password); - Ctx.INSTANCE.put(PASSWORD, str); + public SecretPassword(final char[] password) { // NOPMD - explicit array ownership is part of the API contract + this(password, false); + } + + private SecretPassword(final char[] password, final boolean takeOwnership) { + Objects.requireNonNull(password, "password must not be null"); + this.password = takeOwnership ? password : password.clone(); + } + + /** + * Returns an owned copy of the password characters. + * + * @return a newly allocated password copy + * @throws IllegalStateException if this password has been destroyed + */ + public char[] chars() { + lifecycleLock.lock(); + try { + ensureActive(); + return password.clone(); + } finally { + lifecycleLock.unlock(); + } + } + + /** + * Returns a UTF-8 encoding owned by the caller. + * + * @return a newly allocated UTF-8 byte array + * @throws IllegalStateException if this password has been destroyed + */ + @Override + public byte[] toBytes() { + lifecycleLock.lock(); + try { + ensureActive(); + ByteBuffer encoded = null; + try { + encoded = StandardCharsets.UTF_8.newEncoder().encode(CharBuffer.wrap(password)); + final byte[] result = new byte[encoded.remaining()]; + encoded.get(result); + return result; + } catch (CharacterCodingException exception) { + throw new IllegalStateException("Password could not be encoded", exception); + } finally { + if (encoded != null && encoded.hasArray()) { + Arrays.fill(encoded.array(), (byte) 0); + } + } + } finally { + lifecycleLock.unlock(); + } + } + + /** + * Returns a stream over a private UTF-8 buffer. Closing the stream wipes that + * buffer. + * + * @return a stream that wipes its buffer when closed + * @throws IllegalStateException if this password has been destroyed + */ + @Override + public InputStream getStream() { + lifecycleLock.lock(); + try { + ensureActive(); + return new DestroyingByteArrayInputStream(toBytes()); + } finally { + lifecycleLock.unlock(); + } + } + + /** + * Returns a redacted diagnostic representation. + * + * @return {@code "[REDACTED]"} + */ + @Override + public String toText() { + return REDACTED; + } + + /** + * Returns a redacted diagnostic representation. + * + * @return {@code "[REDACTED]"} + */ + @Override + public String toString() { + return REDACTED; + } + + /** + * Rejects preceding content because a password is a chain source. + * + * @param input preceding content; must be {@code null} + * @throws IllegalArgumentException if {@code input} is non-null + */ + @Override + public void setInput(DataContent input) { + if (input != null) { + throw new IllegalArgumentException("SecretPassword must be the first content element"); + } + } + + /** {@inheritDoc} */ + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(password, '\0'); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + /** {@inheritDoc} */ + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("Password has been destroyed"); + } + } + + /** + * Byte-array stream that wipes its owned storage when closed. + */ + private static final class DestroyingByteArrayInputStream extends ByteArrayInputStream { + private final byte[] ownedBuffer; + private boolean closed; + + private DestroyingByteArrayInputStream(final byte[] buffer) { + super(buffer); + ownedBuffer = buffer; + } + + @Override + public void close() { + if (!closed) { + Arrays.fill(ownedBuffer, (byte) 0); + closed = true; + } + } } } diff --git a/lib/src/main/java/zeroecho/sdk/content/builtin/package-info.java b/lib/src/main/java/zeroecho/sdk/content/builtin/package-info.java index b36530c..771c745 100644 --- a/lib/src/main/java/zeroecho/sdk/content/builtin/package-info.java +++ b/lib/src/main/java/zeroecho/sdk/content/builtin/package-info.java @@ -50,9 +50,8 @@ * demand.
      • *
      • {@link PlainFile} - reads content from a {@link java.net.URL} such as * {@code file:} or {@code https:}.
      • - *
      • {@link SecretPassword} - specialization for secret phrases; extends - * {@link PlainString} and publishes the password into a process context for - * downstream consumers when constructed.
      • + *
      • {@link SecretPassword} - destroyable secret content backed by an owned + * character array. Its inherited string representation is always redacted.
      • *
      * *

      Behavior

      @@ -82,8 +81,14 @@ * in.transferTo(out); * } * - * // Use a password source (also published to a shared context by the constructor). - * zeroecho.sdk.content.api.DataContent secret = new zeroecho.sdk.content.builtin.SecretPassword(24); + * // Use a password source and destroy it after consumption. + * zeroecho.sdk.content.builtin.SecretPassword secret = + * new zeroecho.sdk.content.builtin.SecretPassword(24); + * try (java.io.InputStream in = secret.getStream()) { + * in.transferTo(out); + * } finally { + * secret.destroy(); + * } * }
      * * @since 1.0 diff --git a/lib/src/main/java/zeroecho/sdk/guard/Decryptor.java b/lib/src/main/java/zeroecho/sdk/guard/Decryptor.java index dbc40fb..8b29b09 100644 --- a/lib/src/main/java/zeroecho/sdk/guard/Decryptor.java +++ b/lib/src/main/java/zeroecho/sdk/guard/Decryptor.java @@ -36,6 +36,7 @@ package zeroecho.sdk.guard; import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.logging.Level; @@ -43,6 +44,7 @@ import java.util.logging.Logger; import javax.crypto.AEADBadTagException; import javax.crypto.spec.SecretKeySpec; +import javax.security.auth.DestroyFailedException; import zeroecho.core.io.Util; import zeroecho.sdk.builders.alg.AesDataContentBuilder; @@ -54,7 +56,7 @@ import zeroecho.sdk.content.api.PlainContent; * Decrypting stage that scans recipient entries to recover the CEK and then * delegates to the symmetric builder. */ -final class Decryptor implements PlainContent { +final class Decryptor implements PlainContent, MultiRecipientContent { private static final Logger LOG = Logger.getLogger(Decryptor.class.getName()); private final List openers; @@ -65,6 +67,7 @@ final class Decryptor implements PlainContent { private final int maxRecipients; private final int maxEntryLen; private DataContent upstream; + private boolean closed; /* package */ Decryptor(List openers, UnlockMaterial material, AesDataContentBuilder aesBuilder, ChaChaDataContentBuilder chachaBuilder, int keyBytes, int maxRecipients, int maxEntryLen) { @@ -85,6 +88,7 @@ final class Decryptor implements PlainContent { */ @Override public void setInput(DataContent input) { + ensureOpen(); this.upstream = Objects.requireNonNull(input); } @@ -99,81 +103,223 @@ final class Decryptor implements PlainContent { */ @Override public InputStream getStream() throws IOException { // NOPMD - Objects.requireNonNull(upstream, "decrypt: missing input"); - InputStream in = upstream.getStream(); // NOPMD - - // 1) Read recipients - int count = Util.readPack7I(in); - if (count < 0 || count > maxRecipients) { - in.close(); - throw new IOException("invalid recipient count: " + count); - } - + ensureOpen(); + InputStream in = null; // NOPMD String matchedId = null; byte[] cek = null; - - for (int i = 0; i < count; i++) { - String id = Util.readUTF8(in, 256); // reasonable max id length - byte[] blob = Util.read(in, maxEntryLen); - - if (LOG.isLoggable(Level.FINE)) { - LOG.log(Level.FINE, "round {0} id {1} material {2}", new Object[] { i, id, material.description() }); // NOPMD + Throwable failure = null; + boolean transferred = false; + try { + Objects.requireNonNull(upstream, "decrypt: missing input"); + in = upstream.getStream(); + // 1) Read recipients + int count = Util.readPack7I(in); + if (count < 0 || count > maxRecipients) { + throw new IOException("invalid recipient count: " + count); } - for (RecipientOpener op : openers) { + for (int i = 0; i < count; i++) { + String id = Util.readUTF8(in, 256); // reasonable max id length + byte[] blob = Util.read(in, maxEntryLen); + if (LOG.isLoggable(Level.FINE)) { - LOG.log(Level.FINE, "testing {0}", op.getClass().getName()); + LOG.log(Level.FINE, "round {0} id {1} material {2}", + new Object[] { i, id, material.description() }); // NOPMD } - try { - byte[] maybe = op.tryOpen(id, blob, material); - if (maybe != null) { - if (maybe.length > keyBytes) { - if (LOG.isLoggable(Level.WARNING)) { // NOPMD - LOG.log(Level.WARNING, - "Suspicious material in field {0}: {1}/{2} finds the secret of length {3}, while {4} is a limit. Ignoring.", - new Object[] { i, id, op.toString(), maybe.length, keyBytes }); // NOPMD + + for (RecipientOpener op : openers) { // NOPMD - owned openers close in closeAfterAttempt + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "testing {0}", op.getClass().getName()); + } + try { + byte[] maybe = op.tryOpen(id, blob, material); + if (maybe != null) { + if (maybe.length > keyBytes) { + rejectOversizedCek(maybe, i, id, op); + } else { + cek = maybe; + matchedId = id; + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "*** match with {0} ***", op.getClass().getName()); + } + break; } - } else { - cek = maybe; - matchedId = id; - if (LOG.isLoggable(Level.FINE)) { - LOG.log(Level.FINE, "*** match with {0} ***", op.getClass().getName()); - } - break; + } + } catch (AEADBadTagException ex) { + // wrong key/password for that entry, continue scanning + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "recipient authentication failed: {0}", + ex.getClass().getSimpleName()); + } + } catch (GeneralSecurityException | IOException | IllegalArgumentException ex) { + // entry not applicable to this opener/material; ignore and continue + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "recipient opener failed: {0}", ex.getClass().getSimpleName()); } } - } catch (AEADBadTagException ex) { - // wrong key/password for that entry, continue scanning - LOG.log(Level.FINE, "failed AEAD {0}", ex); - } catch (GeneralSecurityException | IOException | IllegalArgumentException ex) { - // entry not applicable to this opener/material; ignore and continue - LOG.log(Level.FINE, "failed {0}", ex); + } + if (cek != null) { + // Skip remaining recipient entries if any + for (int j = i + 1; j < count; j++) { + Util.readUTF8(in, 256); + Util.read(in, maxEntryLen); + } + break; } } + + if (cek == null) { + throw new IOException("unable to unlock CEK with provided material"); + } + + LOG.log(Level.INFO, "found={0}", matchedId); + + close(); + + // 2) Build symmetric decrypt stage and feed the remaining stream + final DataContent symmetric; + if (aesBuilder != null) { + symmetric = aesBuilder.withKey(new SecretKeySpec(cek, "AES")).build(false); + } else { + symmetric = chachaBuilder.withKey(new SecretKeySpec(cek, "ChaCha20")).build(false); + } + symmetric.setInput(new TailDataContent(in)); + InputStream result = symmetric.getStream(); + transferred = true; + return result; + } catch (IOException | RuntimeException | Error exception) { // NOPMD - retain primary failure + failure = exception; + throw exception; + } finally { if (cek != null) { - // Skip remaining recipient entries if any - for (int j = i + 1; j < count; j++) { - Util.readUTF8(in, 256); - Util.read(in, maxEntryLen); - } - break; + Arrays.fill(cek, (byte) 0); + } + closeAfterAttempt(in, transferred, failure); + } + } + + private void closeAfterAttempt(InputStream input, boolean transferred, Throwable primary) + throws IOException { + IOException cleanupFailure = null; + if (!transferred) { + try { + closeRejectedInput(input, primary); + } catch (IOException exception) { + cleanupFailure = exception; } } - - if (cek == null) { - throw new IOException("unable to unlock CEK with provided material"); + Throwable effectivePrimary = primary == null ? cleanupFailure : primary; + try { + closeWithPrimary(effectivePrimary); + } catch (IOException exception) { + cleanupFailure = exception; } - - LOG.log(Level.INFO, "found={0}", matchedId); - - // 2) Build symmetric decrypt stage and feed the remaining stream - final DataContent symmetric; - if (aesBuilder != null) { - symmetric = aesBuilder.withKey(new SecretKeySpec(cek, "AES")).build(false); - } else { - symmetric = chachaBuilder.withKey(new SecretKeySpec(cek, "ChaCha20")).build(false); + if (primary == null && cleanupFailure != null) { + throw cleanupFailure; + } + } + + private static void closeRejectedInput(InputStream input, Throwable primary) throws IOException { + if (input == null) { + return; + } + try { + input.close(); + } catch (IOException cleanupFailure) { + if (primary != null) { + primary.addSuppressed(cleanupFailure); + } else { + throw cleanupFailure; + } + } + } + + /** + * Releases opener-owned resources after scanning or abandonment. + * + * @throws IOException if opener cleanup fails + */ + @Override + public void close() throws IOException { + if (closed) { + return; + } + closed = true; + closeOpeners(openers, null); + } + + /** + * Destroys opener-owned resources after scanning or abandonment. + * + * @throws DestroyFailedException if opener cleanup fails + */ + @Override + public void destroy() throws DestroyFailedException { + try { + close(); + } catch (IOException exception) { + DestroyFailedException failure = new DestroyFailedException("Recipient opener cleanup failed"); + failure.initCause(exception); + throw failure; + } + } + + /** + * Reports whether opener-owned resources have been released. + * + * @return {@code true} after processing or explicit cleanup + */ + @Override + public boolean isDestroyed() { + return closed; + } + + private void closeWithPrimary(Throwable primary) throws IOException { + if (closed) { + return; + } + closed = true; + closeOpeners(openers, primary); + } + + /* default */ static void closeOpeners(List ownedOpeners, Throwable primary) + throws IOException { + IOException cleanupFailure = null; + for (RecipientOpener opener : ownedOpeners) { // NOPMD - each opener is closed in this loop + try { + opener.close(); + } catch (IOException wrapped) { + if (primary != null) { + primary.addSuppressed(wrapped); + } else if (cleanupFailure == null) { + cleanupFailure = wrapped; + } else { + cleanupFailure.addSuppressed(wrapped); + } + } + } + if (primary == null && cleanupFailure != null) { + throw cleanupFailure; + } + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("Multi-recipient decrypting content has been closed"); + } + } + + private void rejectOversizedCek(byte[] candidate, int fieldIndex, String recipientId, + RecipientOpener opener) { + try { + if (LOG.isLoggable(Level.WARNING)) { + LOG.log(Level.WARNING, + "Suspicious material in field {0}: {1}/{2} returned length {3}, while {4} is the limit. Ignoring.", + new Object[] { fieldIndex, recipientId, opener.getClass().getName(), + candidate.length, keyBytes }); + } + } finally { + Arrays.fill(candidate, (byte) 0); } - symmetric.setInput(new TailDataContent(in)); - return symmetric.getStream(); } } diff --git a/lib/src/main/java/zeroecho/sdk/guard/EncCtxOpener.java b/lib/src/main/java/zeroecho/sdk/guard/EncCtxOpener.java index 6feb1f8..98d162b 100644 --- a/lib/src/main/java/zeroecho/sdk/guard/EncCtxOpener.java +++ b/lib/src/main/java/zeroecho/sdk/guard/EncCtxOpener.java @@ -33,6 +33,8 @@ ******************************************************************************/ package zeroecho.sdk.guard; +import zeroecho.sdk.ZeroEchoSession; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -40,30 +42,29 @@ import java.io.InputStream; import java.security.GeneralSecurityException; import java.util.Objects; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.KeyUsage; import zeroecho.core.context.EncryptionContext; /** - * Opener that recognizes "CTX-ENC:<algId>" entries and attempts CEK - * recovery using an {@link EncryptionContext} in DECRYPT role. If constructed - * without a context, it derives one from the entryId and the supplied private - * key. + * Reusable opener for {@code CTX-ENC:} entries. + * + *

      + * Each applicable recipient entry receives a fresh decryption context from the + * explicit session. A failed same-algorithm entry therefore cannot consume the + * opener state needed by a later legitimate entry. + *

      */ public final class EncCtxOpener implements RecipientOpener { - private final EncryptionContext provided; // may be null - - /** Derive the context on demand from entryId and private key. */ - public EncCtxOpener() { - this.provided = null; - } + private final ZeroEchoSession session; /** - * Use the provided context once for the matching entry. The opener will consume - * and close this context when used. + * Creates an opener that derives contexts through the explicit session. + * + * @param session runtime configuration used for context creation + * @throws NullPointerException if {@code session} is {@code null} */ - public EncCtxOpener(EncryptionContext ctx) { - this.provided = Objects.requireNonNull(ctx); + public EncCtxOpener(ZeroEchoSession session) { + this.session = Objects.requireNonNull(session, "session"); } @Override @@ -73,23 +74,22 @@ public final class EncCtxOpener implements RecipientOpener { return null; // NOPMD } - final EncryptionContext dec; - if (provided != null) { - dec = provided; - } else { - if (!(material instanceof UnlockMaterial.Private)) { - return null; // NOPMD - } - final String algId = entryId.substring("CTX-ENC:".length()); - final java.security.PrivateKey priv = ((UnlockMaterial.Private) material).key(); - dec = CryptoAlgorithms.create(algId, KeyUsage.DECRYPT, priv); + if (!(material instanceof UnlockMaterial.Private)) { + return null; // NOPMD } + String algorithmId = entryId.substring("CTX-ENC:".length()); + java.security.PrivateKey privateKey = ((UnlockMaterial.Private) material).key(); + try (EncryptionContext context = session.createContext(algorithmId, KeyUsage.DECRYPT, privateKey)) { + return openWithContext(context, blob); + } + } - try (dec; - InputStream s = dec.attach(new ByteArrayInputStream(blob)); + private static byte[] openWithContext(EncryptionContext context, byte[] blob) throws IOException { + try (InputStream s = context.attach(new ByteArrayInputStream(blob)); ByteArrayOutputStream out = new ByteArrayOutputStream()) { s.transferTo(out); return out.toByteArray(); } } + } diff --git a/lib/src/main/java/zeroecho/sdk/guard/EncCtxRecipient.java b/lib/src/main/java/zeroecho/sdk/guard/EncCtxRecipient.java index b1a04cb..5b55926 100644 --- a/lib/src/main/java/zeroecho/sdk/guard/EncCtxRecipient.java +++ b/lib/src/main/java/zeroecho/sdk/guard/EncCtxRecipient.java @@ -57,9 +57,10 @@ import zeroecho.core.context.EncryptionContext; * * @since 1.0 */ -public final class EncCtxRecipient implements Recipient { +public final class EncCtxRecipient implements Recipient, AutoCloseable { private final EncryptionContext enc; private final boolean decoy; + private boolean closed; /** * Constructs a recipient that uses the given {@link EncryptionContext}. @@ -121,10 +122,15 @@ public final class EncCtxRecipient implements Recipient { */ @Override public byte[] buildRecipientEntry(byte[] cek) throws IOException { + if (closed) { + throw new IllegalStateException("Encryption recipient has been closed"); + } ByteArrayInputStream in = new ByteArrayInputStream(cek); - try (enc; InputStream s = enc.attach(in); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + try (InputStream s = enc.attach(in); ByteArrayOutputStream out = new ByteArrayOutputStream()) { s.transferTo(out); return out.toByteArray(); + } finally { + close(); } } @@ -145,4 +151,17 @@ public final class EncCtxRecipient implements Recipient { public boolean decoy() { return decoy; } + + /** + * Closes the owned encryption context. + * + * @throws IOException if context cleanup fails + */ + @Override + public void close() throws IOException { + if (!closed) { + closed = true; + enc.close(); + } + } } diff --git a/lib/src/main/java/zeroecho/sdk/guard/Encryptor.java b/lib/src/main/java/zeroecho/sdk/guard/Encryptor.java index ce33819..f17ab2b 100644 --- a/lib/src/main/java/zeroecho/sdk/guard/Encryptor.java +++ b/lib/src/main/java/zeroecho/sdk/guard/Encryptor.java @@ -39,10 +39,13 @@ import java.io.IOException; import java.io.InputStream; import java.io.SequenceInputStream; import java.security.GeneralSecurityException; +import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.function.IntFunction; import javax.crypto.spec.SecretKeySpec; +import javax.security.auth.DestroyFailedException; import zeroecho.core.io.Util; import zeroecho.sdk.builders.alg.AesDataContentBuilder; @@ -55,23 +58,34 @@ import zeroecho.sdk.util.RandomSupport; * Encrypting stage that emits the recipient table followed by the symmetric * builder output. */ -final class Encryptor implements EncryptedContent { +final class Encryptor implements EncryptedContent, MultiRecipientContent { private final List recipients; private final AesDataContentBuilder aesBuilder; private final ChaChaDataContentBuilder chachaBuilder; private final int keyBytes; private final int maxRecipients; private final int maxEntryLen; + private final IntFunction randomBytesFactory; private DataContent upstream; + private boolean closed; /* package */ Encryptor(List recipients, AesDataContentBuilder aesBuilder, ChaChaDataContentBuilder chachaBuilder, int keyBytes, int maxRecipients, int maxEntryLen) { + this(recipients, aesBuilder, chachaBuilder, keyBytes, maxRecipients, maxEntryLen, + RandomSupport::generateRandom); + } + + /* package */ Encryptor(List recipients, AesDataContentBuilder aesBuilder, + ChaChaDataContentBuilder chachaBuilder, int keyBytes, int maxRecipients, int maxEntryLen, + IntFunction randomBytesFactory) { this.recipients = recipients; this.aesBuilder = aesBuilder; this.chachaBuilder = chachaBuilder; this.keyBytes = keyBytes; this.maxRecipients = maxRecipients; this.maxEntryLen = maxEntryLen; + this.randomBytesFactory = Objects.requireNonNull(randomBytesFactory, + "randomBytesFactory must not be null"); } /** @@ -82,6 +96,7 @@ final class Encryptor implements EncryptedContent { */ @Override public void setInput(DataContent input) { + ensureOpen(); this.upstream = Objects.requireNonNull(input); } @@ -95,44 +110,157 @@ final class Encryptor implements EncryptedContent { */ @Override public InputStream getStream() throws IOException { - Objects.requireNonNull(upstream, "encrypt: missing input"); - - // 1) Generate CEK for payload - byte[] cek = RandomSupport.generateRandom(keyBytes); - byte[] cekDecoy = RandomSupport.generateRandom(keyBytes); - - // 2) Build recipient entries - if (recipients.size() > maxRecipients) { - throw new IOException("too many recipients: " + recipients.size()); - } - ByteArrayOutputStream header = new ByteArrayOutputStream(1024); - Util.writePack7I(header, recipients.size()); - for (Recipient r : recipients) { - String id = r.id(); - Util.writeUTF8(header, id); - try { - byte[] blob = r.buildRecipientEntry(r.decoy() ? cekDecoy : cek); - if (blob.length > maxEntryLen) { - throw new IOException("recipient entry too large: " + blob.length); + ensureOpen(); + byte[] cek = null; + byte[] decoyCek = null; + Throwable failure = null; + try { + Objects.requireNonNull(upstream, "encrypt: missing input"); + if (recipients.size() > maxRecipients) { + throw new IOException("too many recipients: " + recipients.size()); + } + cek = requireRandomKey(randomBytesFactory.apply(keyBytes), keyBytes); + try (ByteArrayOutputStream header = new ByteArrayOutputStream(1024)) { + Util.writePack7I(header, recipients.size()); + for (Recipient recipient : recipients) { + String id = recipient.id(); + Util.writeUTF8(header, id); + try { + byte[] recipientCek = cek; + if (recipient.decoy()) { + if (decoyCek == null) { + decoyCek = requireRandomKey(randomBytesFactory.apply(keyBytes), keyBytes); + } + recipientCek = decoyCek; + } + byte[] blob = recipient.buildRecipientEntry(recipientCek); + if (blob.length > maxEntryLen) { + throw new IOException("recipient entry too large: " + blob.length); + } + Util.write(header, blob); + } catch (GeneralSecurityException exception) { + throw new IOException("recipient build failed for " + id, exception); + } + } + + final DataContent symmetric; + if (aesBuilder != null) { + symmetric = aesBuilder.withKey(new SecretKeySpec(cek, "AES")).build(true); + } else { + symmetric = chachaBuilder.withKey(new SecretKeySpec(cek, "ChaCha20")).build(true); + } + symmetric.setInput(upstream); + + InputStream headerStream = new ByteArrayInputStream(header.toByteArray()); + InputStream payloadStream = symmetric.getStream(); + return new SequenceInputStream(headerStream, payloadStream); + } + } catch (IOException | RuntimeException | Error exception) { // NOPMD - retain primary failure + failure = exception; + throw exception; + } finally { + if (cek != null) { + Arrays.fill(cek, (byte) 0); + } + if (decoyCek != null) { + Arrays.fill(decoyCek, (byte) 0); + } + closeWithPrimary(failure); + } + } + + /** + * Releases recipient resources that have not yet been consumed. + * + * @throws IOException if recipient cleanup fails + */ + @Override + public void close() throws IOException { + if (closed) { + return; + } + closed = true; + closeRecipients(recipients, null); + } + + /** + * Destroys recipient resources that have not yet been consumed. + * + * @throws DestroyFailedException if recipient cleanup fails + */ + @Override + public void destroy() throws DestroyFailedException { + try { + close(); + } catch (IOException exception) { + DestroyFailedException failure = new DestroyFailedException("Recipient cleanup failed"); + failure.initCause(exception); + throw failure; + } + } + + /** + * Reports whether recipient resources have been released. + * + * @return {@code true} after processing or explicit cleanup + */ + @Override + public boolean isDestroyed() { + return closed; + } + + private void closeWithPrimary(Throwable primary) throws IOException { + if (closed) { + return; + } + closed = true; + closeRecipients(recipients, primary); + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("Multi-recipient encrypting content has been closed"); + } + } + + private static byte[] requireRandomKey(byte[] key, int expectedLength) { + Objects.requireNonNull(key, "random source returned null"); + if (key.length != expectedLength) { + Arrays.fill(key, (byte) 0); + throw new IllegalStateException("random source returned an invalid key length"); + } + return key; + } + + /* default */ static void closeRecipients(List ownedRecipients, Throwable primary) + throws IOException { + IOException cleanupFailure = null; + for (Recipient recipient : ownedRecipients) { + try { + closeRecipient(recipient); + } catch (IOException wrapped) { + if (primary != null) { + primary.addSuppressed(wrapped); + } else if (cleanupFailure == null) { + cleanupFailure = wrapped; + } else { + cleanupFailure.addSuppressed(wrapped); } - Util.write(header, blob); - } catch (GeneralSecurityException e) { - throw new IOException("recipient build failed for " + id, e); } } - - // 3) Configure symmetric stage (it owns its own header) - final DataContent symmetric; - if (aesBuilder != null) { - symmetric = aesBuilder.withKey(new SecretKeySpec(cek, "AES")).build(true); - } else { - symmetric = chachaBuilder.withKey(new SecretKeySpec(cek, "ChaCha20")).build(true); + if (primary == null && cleanupFailure != null) { + throw cleanupFailure; } - symmetric.setInput(upstream); + } - // 4) Return [recipient header] + [symmetric payload stream] - InputStream headerStream = new ByteArrayInputStream(header.toByteArray()); - InputStream payloadStream = symmetric.getStream(); - return new SequenceInputStream(headerStream, payloadStream); + @SuppressWarnings("PMD.CloseResource") // type-pattern variables are closed immediately in every matching branch + private static void closeRecipient(Recipient recipient) throws IOException { + if (recipient instanceof PasswordRecipient passwordRecipient) { + passwordRecipient.close(); + } else if (recipient instanceof KemCtxRecipient kemRecipient) { + kemRecipient.close(); + } else if (recipient instanceof EncCtxRecipient encryptionRecipient) { + encryptionRecipient.close(); + } } } diff --git a/lib/src/main/java/zeroecho/sdk/guard/KemCtxOpener.java b/lib/src/main/java/zeroecho/sdk/guard/KemCtxOpener.java index 47d21ae..233f8bf 100644 --- a/lib/src/main/java/zeroecho/sdk/guard/KemCtxOpener.java +++ b/lib/src/main/java/zeroecho/sdk/guard/KemCtxOpener.java @@ -33,39 +33,40 @@ ******************************************************************************/ package zeroecho.sdk.guard; +import zeroecho.sdk.ZeroEchoSession; + import java.io.ByteArrayInputStream; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; +import java.util.Arrays; import java.util.Objects; import javax.crypto.AEADBadTagException; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.KeyUsage; import zeroecho.core.context.KemContext; import zeroecho.core.io.Util; -import zeroecho.sdk.util.Kdf; /** - * Opener for "KEM:<algId>:GCM-WRAP" entries. If a context is provided, it - * is used directly; otherwise a context is derived from entryId and the - * supplied private key. + * Reusable opener for {@code KEM::GCM-WRAP} entries. + * + *

      + * Each applicable recipient entry receives a fresh decapsulation context from + * the explicit session. A failed same-algorithm entry therefore cannot consume + * the opener state needed by a later legitimate entry. + *

      */ public final class KemCtxOpener implements RecipientOpener { - private final KemContext provided; // may be null - - /** Derive the context on demand from entryId and private key. */ - public KemCtxOpener() { - this.provided = null; - } + private final ZeroEchoSession session; /** - * Use the provided context once for the matching entry. The opener will consume - * and close this context when used. + * Creates an opener that derives contexts through the explicit session. + * + * @param session runtime configuration used for context creation + * @throws NullPointerException if {@code session} is {@code null} */ - public KemCtxOpener(KemContext kem) { - this.provided = Objects.requireNonNull(kem); + public KemCtxOpener(ZeroEchoSession session) { + this.session = Objects.requireNonNull(session, "session"); } @Override @@ -79,36 +80,42 @@ public final class KemCtxOpener implements RecipientOpener { if (next <= 4) { // NOPMD return null; // NOPMD malformed id } - final String kemId = entryId.substring(4, next); - - final KemContext kem; - if (provided != null) { - // Caller-supplied context (material not required) - kem = provided; - } else { - if (!(material instanceof UnlockMaterial.Private)) { - return null; // NOPMD - } - final java.security.PrivateKey prv = ((UnlockMaterial.Private) material).key(); - kem = CryptoAlgorithms.create(kemId, KeyUsage.DECAPSULATE, prv); + if (!(material instanceof UnlockMaterial.Private)) { + return null; // NOPMD } + final String kemId = entryId.substring(4, next); + java.security.PrivateKey privateKey = ((UnlockMaterial.Private) material).key(); + try (KemContext kem = session.createContext(kemId, KeyUsage.DECAPSULATE, privateKey)) { + return openWithContext(kem, entryBlob); + } + } - try (KemContext c = kem; ByteArrayInputStream in = new ByteArrayInputStream(entryBlob)) { + private static byte[] openWithContext(KemContext kem, byte[] entryBlob) + throws GeneralSecurityException, IOException { + try (ByteArrayInputStream in = new ByteArrayInputStream(entryBlob)) { byte[] kemCt = Util.read(in, 1 << 20); byte[] salt = Util.read(in, 64); byte[] wrapIv = Util.read(in, 64); byte[] wrapped = Util.read(in, 1 << 16); - byte[] ss = c.decapsulate(kemCt); - byte[] info = ("KDF:" + kem.algorithm().id()).getBytes(StandardCharsets.US_ASCII); - byte[] kek = Kdf.hkdfSha256(ss, salt, info, 32); + byte[] ss = kem.decapsulate(kemCt); + byte[] kek = null; try { - return MultiRecipientDataSourceBuilder.aesGcmUnwrap(kek, wrapIv, null, wrapped); - } catch (AEADBadTagException e) { - // Sender could have used 128-bit KEK - kek = Kdf.hkdfSha256(ss, salt, "KEM:KEK".getBytes(StandardCharsets.UTF_8), 16); - return MultiRecipientDataSourceBuilder.aesGcmUnwrap(kek, wrapIv, null, wrapped); + try { + kek = KemKeyDerivation.derive(ss, salt, kem.algorithm().id(), 32); + return MultiRecipientDataSourceBuilder.aesGcmUnwrap(kek, wrapIv, null, wrapped); + } catch (AEADBadTagException exception) { + Arrays.fill(kek, (byte) 0); + kek = KemKeyDerivation.derive(ss, salt, kem.algorithm().id(), 16); + return MultiRecipientDataSourceBuilder.aesGcmUnwrap(kek, wrapIv, null, wrapped); + } + } finally { + Arrays.fill(ss, (byte) 0); + if (kek != null) { + Arrays.fill(kek, (byte) 0); + } } } } + } diff --git a/lib/src/main/java/zeroecho/sdk/guard/KemCtxRecipient.java b/lib/src/main/java/zeroecho/sdk/guard/KemCtxRecipient.java index 87bc3fd..2d37645 100644 --- a/lib/src/main/java/zeroecho/sdk/guard/KemCtxRecipient.java +++ b/lib/src/main/java/zeroecho/sdk/guard/KemCtxRecipient.java @@ -35,13 +35,13 @@ package zeroecho.sdk.guard; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.Objects; import zeroecho.core.context.KemContext; import zeroecho.core.context.KemContext.KemResult; import zeroecho.core.io.Util; -import zeroecho.sdk.util.Kdf; import zeroecho.sdk.util.RandomSupport; /** @@ -68,18 +68,22 @@ import zeroecho.sdk.util.RandomSupport; * * @since 1.0 */ -public final class KemCtxRecipient implements Recipient { +public final class KemCtxRecipient implements Recipient, AutoCloseable { private final KemContext ctx; private final int kekBytes; private final int saltLen; private final boolean decoy; + private boolean closed; /** * Constructs a recipient that uses the given KEM context and parameters. * * @param ctx the KEM context providing encapsulation - * @param kekBytes number of bytes of KEK material to derive via HKDF + * @param kekBytes KEK length; exactly 16 or 32 bytes * @param saltLen length of the random salt to apply during HKDF + * @throws NullPointerException if {@code ctx} is {@code null} + * @throws IllegalArgumentException if {@code kekBytes} is not exactly 16 or + * 32 */ public KemCtxRecipient(KemContext ctx, int kekBytes, int saltLen) { this(ctx, kekBytes, saltLen, false); @@ -98,14 +102,18 @@ public final class KemCtxRecipient implements Recipient { * * @param ctx the KEM context responsible for performing encapsulation; * must not be {@code null} - * @param kekBytes number of bytes of KEK material to derive via HKDF + * @param kekBytes KEK length; exactly 16 or 32 bytes * @param saltLen length in bytes of the random salt applied during HKDF * @param decoy {@code true} if this recipient is a decoy (fake entry that * cannot unwrap a CEK); {@code false} if it is a real recipient + * @throws NullPointerException if {@code ctx} is {@code null} + * @throws IllegalArgumentException if {@code kekBytes} is not exactly 16 or + * 32 */ public KemCtxRecipient(KemContext ctx, int kekBytes, int saltLen, boolean decoy) { - this.ctx = ctx; - this.kekBytes = kekBytes; + int validatedKekBytes = RecipientKekSizes.requireSupported(kekBytes); + this.ctx = Objects.requireNonNull(ctx, "ctx must not be null"); + this.kekBytes = validatedKekBytes; this.saltLen = saltLen; this.decoy = decoy; } @@ -142,25 +150,35 @@ public final class KemCtxRecipient implements Recipient { */ @Override public byte[] buildRecipientEntry(byte[] cek) throws GeneralSecurityException, IOException { - try (ctx) { + if (closed) { + throw new IllegalStateException("KEM recipient has been closed"); + } + try { KemResult kr = ctx.encapsulate(); byte[] kemCt = kr.ciphertext(); byte[] ss = kr.sharedSecret(); + byte[] kek = null; + try { + byte[] salt = RandomSupport.generateRandom(saltLen); + kek = KemKeyDerivation.derive(ss, salt, ctx.algorithm().id(), kekBytes); + byte[] iv = RandomSupport.generateRandom(12); + byte[] wrapped = MultiRecipientDataSourceBuilder.aesGcmWrap(kek, iv, null, cek); - byte[] salt = RandomSupport.generateRandom(saltLen); - byte[] info = ("KDF:" + ctx.algorithm().id()).getBytes(StandardCharsets.US_ASCII); - byte[] kek = Kdf.hkdfSha256(ss, salt, info, kekBytes); - - byte[] iv = RandomSupport.generateRandom(12); - byte[] wrapped = MultiRecipientDataSourceBuilder.aesGcmWrap(kek, iv, null, cek); - - ByteArrayOutputStream out = new ByteArrayOutputStream( - kemCt.length + salt.length + iv.length + wrapped.length + 8); - Util.write(out, kemCt); - Util.write(out, salt); - Util.write(out, iv); - Util.write(out, wrapped); - return out.toByteArray(); + ByteArrayOutputStream out = new ByteArrayOutputStream( + kemCt.length + salt.length + iv.length + wrapped.length + 8); + Util.write(out, kemCt); + Util.write(out, salt); + Util.write(out, iv); + Util.write(out, wrapped); + return out.toByteArray(); + } finally { + Arrays.fill(ss, (byte) 0); + if (kek != null) { + Arrays.fill(kek, (byte) 0); + } + } + } finally { + close(); } } @@ -181,4 +199,17 @@ public final class KemCtxRecipient implements Recipient { public boolean decoy() { return decoy; } + + /** + * Closes the owned KEM context. + * + * @throws IOException if context cleanup fails + */ + @Override + public void close() throws IOException { + if (!closed) { + closed = true; + ctx.close(); + } + } } diff --git a/lib/src/main/java/zeroecho/sdk/guard/KemKeyDerivation.java b/lib/src/main/java/zeroecho/sdk/guard/KemKeyDerivation.java new file mode 100644 index 0000000..1d091bf --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/KemKeyDerivation.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.sdk.guard; + +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.Objects; + +import zeroecho.sdk.util.Kdf; + +/** + * Defines the single KEM-recipient KEK derivation contract. + */ +final class KemKeyDerivation { + private static final String INFO_PREFIX = "KDF:"; + + private KemKeyDerivation() { + } + + /** + * Derives a KEK bound to the KEM algorithm identifier. + * + * @param sharedSecret KEM shared secret + * @param salt HKDF salt + * @param algorithmId canonical KEM algorithm identifier + * @param outputBytes requested KEK size + * @return newly allocated KEK bytes + * @throws GeneralSecurityException if HKDF fails + */ + /* default */ static byte[] derive(byte[] sharedSecret, byte[] salt, String algorithmId, int outputBytes) + throws GeneralSecurityException { + Objects.requireNonNull(algorithmId, "algorithmId must not be null"); + byte[] info = (INFO_PREFIX + algorithmId).getBytes(StandardCharsets.US_ASCII); + return Kdf.hkdfSha256(sharedSecret, salt, info, outputBytes); + } +} diff --git a/lib/src/main/java/zeroecho/sdk/guard/MultiRecipientContent.java b/lib/src/main/java/zeroecho/sdk/guard/MultiRecipientContent.java new file mode 100644 index 0000000..69a108f --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/MultiRecipientContent.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.guard; + +import java.io.IOException; +import java.io.InputStream; + +import javax.security.auth.DestroyFailedException; +import javax.security.auth.Destroyable; + +import zeroecho.sdk.content.api.DataContent; + +/** + * Represents one-shot multi-recipient content that owns temporary recipient + * resources until processing or explicit cleanup. + * + *

      + * Callers must invoke {@link #close()} when a built instance is abandoned before + * {@link #getStream()} is called. Successful or failed stream construction also + * releases the owned recipient resources. Unlocking keys and password material + * supplied separately remain caller-owned and are never destroyed by this + * content. + *

      + * + *

      + * Implementations are not thread-safe. Cleanup is idempotent, and content cannot + * be used after cleanup. Calling {@link #getStream()} is terminal even when stream + * construction fails. + *

      + */ +public interface MultiRecipientContent extends DataContent, Destroyable, AutoCloseable { + /** + * Connects the upstream content before the one processing attempt. + * + * @param input non-null upstream plaintext or encrypted content + * @throws NullPointerException if {@code input} is {@code null} + * @throws IllegalStateException if this content has already processed input or + * has been closed + */ + @Override + void setInput(DataContent input); + + /** + * Creates the processed stream and releases all recipient resources. + * + *

      + * This method may be called only once. Recipient resources are released before + * it returns or throws. On success, the caller owns the returned stream and + * must close it; closing this content does not replace closing an already + * returned stream. + *

      + * + * @return the encrypted or decrypted stream + * @throws IOException if envelope processing, cryptographic stream + * creation, or resource cleanup fails + * @throws NullPointerException if no upstream content was supplied + * @throws IllegalStateException if this content has already processed input or + * has been closed + */ + @Override + InputStream getStream() throws IOException; + + /** + * Releases all recipient resources still owned by this content. + * + * @throws IOException if an owned context cannot be closed + */ + @Override + void close() throws IOException; + + /** + * Destroys all recipient resources still owned by this content. + * + * @throws DestroyFailedException if cleanup fails + */ + @Override + void destroy() throws DestroyFailedException; + + /** + * Reports whether this content has released its recipient resources. + * + * @return {@code true} after processing, {@link #close()}, or + * {@link #destroy()} + */ + @Override + boolean isDestroyed(); +} diff --git a/lib/src/main/java/zeroecho/sdk/guard/MultiRecipientDataSourceBuilder.java b/lib/src/main/java/zeroecho/sdk/guard/MultiRecipientDataSourceBuilder.java index 23b8408..dcadd2d 100644 --- a/lib/src/main/java/zeroecho/sdk/guard/MultiRecipientDataSourceBuilder.java +++ b/lib/src/main/java/zeroecho/sdk/guard/MultiRecipientDataSourceBuilder.java @@ -47,13 +47,16 @@ import javax.crypto.SecretKeyFactory; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; +import javax.security.auth.DestroyFailedException; +import javax.security.auth.Destroyable; import zeroecho.core.context.EncryptionContext; import zeroecho.core.context.KemContext; +import zeroecho.sdk.Pbkdf2Limits; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.sdk.builders.alg.AesDataContentBuilder; import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder; import zeroecho.sdk.builders.core.DataContentBuilder; -import zeroecho.sdk.content.api.DataContent; /** * Builds a data content pipeline that supports multiple recipients and @@ -79,7 +82,9 @@ import zeroecho.sdk.content.api.DataContent; * The symmetric stage parses its own header just as in the KEM-oriented * pipeline. */ -public final class MultiRecipientDataSourceBuilder implements DataContentBuilder { +public final class MultiRecipientDataSourceBuilder + implements DataContentBuilder, Destroyable, AutoCloseable { + private final ZeroEchoSession session; // ---- symmetric selection (mirrors KemDataContentBuilder style) ---- private AesDataContentBuilder aesBuilder; private ChaChaDataContentBuilder chachaBuilder; @@ -93,6 +98,7 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder private int payloadKeyBytes = 32; // e.g., 32 for AES-256 or ChaCha20 private int maxRecipients = 64; private int maxEntryLen = 1 << 20; // up to 1 MiB per recipient entry + private boolean closed; /** * Creates a new builder for constructing a multi-recipient data source. @@ -112,8 +118,12 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder * @return a fresh {@code MultiRecipientDataSourceBuilder} instance with default * settings */ - public static MultiRecipientDataSourceBuilder builder() { - return new MultiRecipientDataSourceBuilder(); + public static MultiRecipientDataSourceBuilder builder(ZeroEchoSession session) { + return new MultiRecipientDataSourceBuilder(session); + } + + private MultiRecipientDataSourceBuilder(ZeroEchoSession session) { + this.session = Objects.requireNonNull(session, "session must not be null"); } /** @@ -129,6 +139,7 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder * @throws NullPointerException if {@code builder} is {@code null} */ public MultiRecipientDataSourceBuilder withAes(AesDataContentBuilder builder) { + ensureOpen(); this.aesBuilder = Objects.requireNonNull(builder, "aesBuilder"); this.chachaBuilder = null; return this; @@ -139,6 +150,7 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder * randomness. All permutations occur with approximately equal likelihood. */ public void shuffle() { + ensureOpen(); Collections.shuffle(recipients); } @@ -156,6 +168,7 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder * @throws NullPointerException if {@code builder} is {@code null} */ public MultiRecipientDataSourceBuilder withChaCha(ChaChaDataContentBuilder builder) { + ensureOpen(); this.chachaBuilder = Objects.requireNonNull(builder, "chachaBuilder"); this.aesBuilder = null; return this; @@ -174,6 +187,7 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder * @throws IllegalArgumentException if {@code keyBytes} is not positive */ public MultiRecipientDataSourceBuilder payloadKeyBytes(int keyBytes) { + ensureOpen(); if (keyBytes <= 0) { throw new IllegalArgumentException("payloadKeyBytes must be > 0"); } @@ -186,15 +200,23 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder * and AES-GCM-wraps the CEK. * * @param password the password; the caller should clear the array after use - * @param iterations PBKDF2 iteration count + * @param iterations PBKDF2 iteration count; must be at least + * {@value Pbkdf2Limits#MINIMUM} * @param saltLen salt length in bytes - * @param kekBytes derived KEK length in bytes (for example, 16 or 32) + * @param kekBytes derived KEK length; exactly 16 or 32 bytes * @return this builder - * @throws NullPointerException if {@code password} is {@code null} + * @throws NullPointerException if {@code password} is {@code null} + * @throws IllegalArgumentException if {@code iterations} is below + * {@value Pbkdf2Limits#MINIMUM} or + * {@code kekBytes} is not exactly 16 or 32 */ public MultiRecipientDataSourceBuilder addPasswordRecipient(char[] password, int iterations, int saltLen, int kekBytes) { - this.recipients.add(new PasswordRecipient(password, iterations, saltLen, kekBytes)); + ensureOpen(); + RecipientKekSizes.requireSupported(kekBytes); + session.pbkdf2Limits().validateTrusted(iterations); + this.recipients.add(new PasswordRecipient(password, iterations, saltLen, kekBytes, false, + session.pbkdf2Limits())); return this; } @@ -210,12 +232,16 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder * * @param kem configured context for * {@link zeroecho.core.KeyUsage#ENCAPSULATE} - * @param kekBytes derived KEK length in bytes (e.g., 16 or 32) + * @param kekBytes derived KEK length; exactly 16 or 32 bytes * @param saltLen HKDF salt length in bytes * @return this builder - * @throws NullPointerException if {@code kem} is {@code null} + * @throws NullPointerException if {@code kem} is {@code null} + * @throws IllegalArgumentException if {@code kekBytes} is not exactly 16 or + * 32 */ public MultiRecipientDataSourceBuilder addRecipient(KemContext kem, int kekBytes, int saltLen) { + ensureOpen(); + RecipientKekSizes.requireSupported(kekBytes); this.recipients.add(new KemCtxRecipient(Objects.requireNonNull(kem), kekBytes, saltLen)); return this; } @@ -241,6 +267,7 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder * @throws NullPointerException if {@code ctx} is {@code null} */ public MultiRecipientDataSourceBuilder addRecipient(EncryptionContext ctx) { + ensureOpen(); this.recipients.add(new EncCtxRecipient(Objects.requireNonNull(ctx))); return this; } @@ -250,15 +277,23 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder * PBKDF2(HMAC-SHA-256) and AES-GCM-wraps the CEK. * * @param password the password; the caller should clear the array after use - * @param iterations PBKDF2 iteration count + * @param iterations PBKDF2 iteration count; must be at least + * {@value Pbkdf2Limits#MINIMUM} * @param saltLen salt length in bytes - * @param kekBytes derived KEK length in bytes (for example, 16 or 32) + * @param kekBytes derived KEK length; exactly 16 or 32 bytes * @return this builder - * @throws NullPointerException if {@code password} is {@code null} + * @throws NullPointerException if {@code password} is {@code null} + * @throws IllegalArgumentException if {@code iterations} is below + * {@value Pbkdf2Limits#MINIMUM} or + * {@code kekBytes} is not exactly 16 or 32 */ public MultiRecipientDataSourceBuilder addPasswordRecipientDecoy(char[] password, int iterations, int saltLen, int kekBytes) { - this.recipients.add(new PasswordRecipient(password, iterations, saltLen, kekBytes, true)); + ensureOpen(); + RecipientKekSizes.requireSupported(kekBytes); + session.pbkdf2Limits().validateTrusted(iterations); + this.recipients.add(new PasswordRecipient(password, iterations, saltLen, kekBytes, true, + session.pbkdf2Limits())); return this; } @@ -275,12 +310,16 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder * * @param kem configured context for * {@link zeroecho.core.KeyUsage#ENCAPSULATE} - * @param kekBytes derived KEK length in bytes (e.g., 16 or 32) + * @param kekBytes derived KEK length; exactly 16 or 32 bytes * @param saltLen HKDF salt length in bytes * @return this builder - * @throws NullPointerException if {@code kem} is {@code null} + * @throws NullPointerException if {@code kem} is {@code null} + * @throws IllegalArgumentException if {@code kekBytes} is not exactly 16 or + * 32 */ public MultiRecipientDataSourceBuilder addRecipientDecoy(KemContext kem, int kekBytes, int saltLen) { + ensureOpen(); + RecipientKekSizes.requireSupported(kekBytes); this.recipients.add(new KemCtxRecipient(Objects.requireNonNull(kem), kekBytes, saltLen, true)); return this; } @@ -306,6 +345,7 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder * @throws NullPointerException if {@code ctx} is {@code null} */ public MultiRecipientDataSourceBuilder addRecipientDecoy(EncryptionContext ctx) { + ensureOpen(); this.recipients.add(new EncCtxRecipient(Objects.requireNonNull(ctx), true)); return this; } @@ -317,13 +357,18 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder *

      * Only a single unlocking material is used. During decryption each opener * attempts to recover the CEK from the recipient entries using this material. + * The builder borrows the object for the lifetime of the built decrypting + * content and never destroys it. The caller retains ownership and must destroy + * password material after the returned content and stream are no longer used. *

      * - * @param material the unlocking material (private key or password) + * @param material caller-owned unlocking material borrowed until decryption + * completes * @return this builder * @throws NullPointerException if {@code material} is {@code null} */ public MultiRecipientDataSourceBuilder unlockWith(UnlockMaterial material) { + ensureOpen(); this.unlockMaterial = Objects.requireNonNull(material); return this; } @@ -338,55 +383,22 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder * password recipients. *

      * - * @param opener the opener to add + *

      + * The builder takes ownership of the opener. The opener must be reusable + * across all recipient entries and is closed after scanning or when the built + * content is abandoned. + *

      + * + * @param opener reusable opener to add * @return this builder * @throws NullPointerException if {@code opener} is {@code null} */ public MultiRecipientDataSourceBuilder addOpener(RecipientOpener opener) { + ensureOpen(); this.openers.add(Objects.requireNonNull(opener)); return this; } - /** - * Adds a universal opener backed by a caller-supplied {@link KemContext}. - * - *

      - * The opener matches entries whose id starts with {@code "KEM:"} and whose - * algorithm id equals {@code kem.algorithm().id()}. When a match is attempted, - * the context is used to {@link KemContext#decapsulate(byte[])} and then - * closed. - *

      - * - * @param kem context configured for {@link zeroecho.core.KeyUsage#DECAPSULATE} - * @return this builder - * @throws NullPointerException if {@code kem} is {@code null} - */ - public MultiRecipientDataSourceBuilder addOpener(KemContext kem) { - this.openers.add(new KemCtxOpener(Objects.requireNonNull(kem))); - return this; - } - - /** - * Adds a universal opener backed by a caller-supplied - * {@link EncryptionContext}. - * - *

      - * The opener matches entries whose id equals - * {@code "ENC:" + ctx.algorithm().id()}. When a match is attempted, the CEK - * ciphertext blob is streamed through - * {@link EncryptionContext#attach(java.io.InputStream)} to obtain the plaintext - * CEK. The context is then closed. - *

      - * - * @param ctx context configured for {@link zeroecho.core.KeyUsage#DECRYPT} - * @return this builder - * @throws NullPointerException if {@code ctx} is {@code null} - */ - public MultiRecipientDataSourceBuilder addOpener(EncryptionContext ctx) { - this.openers.add(new EncCtxOpener(Objects.requireNonNull(ctx))); - return this; - } - /** * Applies defensive limits for parsing the recipient table. * @@ -400,6 +412,7 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder * @return this builder */ public MultiRecipientDataSourceBuilder headerLimits(int maxRecipients, int maxEntryLen) { + ensureOpen(); this.maxRecipients = Math.max(1, maxRecipients); this.maxEntryLen = Math.max(1, maxEntryLen); return this; @@ -418,20 +431,23 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder * *

      * Default openers are installed automatically if none were added explicitly. - * They support KEM, RSA-OAEP, ElGamal with PKCS1 padding, and PBKDF2 password - * recipients. + * They support KEM, RSA-OAEP, ElGamal with PKCS1 padding, and, when password + * unlock material is selected, PBKDF2 password recipients. Password decryption + * requires explicit session PBKDF2 limits. *

      * * @param encrypt {@code true} to build an encrypting content source, * {@code false} to build a decrypting one - * @return an encrypting or decrypting {@link DataContent} wrapper + * @return an encrypting or decrypting one-shot content wrapper; callers must + * close an abandoned result * @throws IllegalStateException if no symmetric builder is selected, if * encryption is requested with no recipients, or * if decryption is requested without unlock * material */ @Override - public DataContent build(boolean encrypt) { + public MultiRecipientContent build(boolean encrypt) { + ensureOpen(); if (aesBuilder == null && chachaBuilder == null) { throw new IllegalStateException("No symmetric builder selected. Call withAes(...) or withChaCha(...)."); } @@ -441,15 +457,88 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder if (!encrypt && unlockMaterial == null) { throw new IllegalStateException("Missing unlock material for decryption"); } - if (openers.isEmpty()) { - openers.add(new EncCtxOpener()); - openers.add(new KemCtxOpener()); - openers.add(new PasswordOpener()); + if (!encrypt && openers.isEmpty()) { + openers.add(new EncCtxOpener(session)); + openers.add(new KemCtxOpener(session)); + if (unlockMaterial instanceof UnlockMaterial.Password) { + openers.add(new PasswordOpener(session.pbkdf2Limits())); + } + } + if (encrypt) { + List transferredRecipients = List.copyOf(recipients); + recipients.clear(); + return new Encryptor(transferredRecipients, aesBuilder, chachaBuilder, payloadKeyBytes, maxRecipients, + maxEntryLen); + } + List transferredOpeners = List.copyOf(openers); + openers.clear(); + return new Decryptor(transferredOpeners, unlockMaterial, aesBuilder, chachaBuilder, payloadKeyBytes, + maxRecipients, maxEntryLen); + } + + /** + * Destroys recipient secrets still owned by this builder. + * + *

      Recipients transferred to a successfully built encrypting content object + * are owned and destroyed by that object instead.

      + * + * @throws DestroyFailedException if recipient cleanup fails + */ + @Override + public void destroy() throws DestroyFailedException { + try { + close(); + } catch (IOException exception) { + DestroyFailedException failure = new DestroyFailedException("Recipient cleanup failed"); + failure.initCause(exception); + throw failure; + } + } + + /** + * Reports whether this builder has released its owned recipient secrets. + * + * @return {@code true} after {@link #close()} or {@link #destroy()} + */ + @Override + public boolean isDestroyed() { + return closed; + } + + /** + * Releases recipient and opener resources not transferred by + * {@link #build(boolean)}. + * + * @throws IOException if recipient cleanup fails + */ + @Override + public void close() throws IOException { + if (closed) { + return; + } + closed = true; + IOException failure = null; + try { + Encryptor.closeRecipients(recipients, null); + } catch (IOException exception) { + failure = exception; + } finally { + recipients.clear(); + } + try { + Decryptor.closeOpeners(openers, failure); + } finally { + openers.clear(); + } + if (failure != null) { + throw failure; + } + } + + private void ensureOpen() { + if (closed) { + throw new IllegalStateException("Multi-recipient builder has been closed"); } - return encrypt - ? new Encryptor(recipients, aesBuilder, chachaBuilder, payloadKeyBytes, maxRecipients, maxEntryLen) - : new Decryptor(openers, unlockMaterial, aesBuilder, chachaBuilder, payloadKeyBytes, maxRecipients, - maxEntryLen); } // ======================================================================== @@ -481,7 +570,11 @@ public final class MultiRecipientDataSourceBuilder implements DataContentBuilder /* default */ static byte[] pbkdf2HmacSha256(char[] password, byte[] salt, int iterations, int outLen) throws GeneralSecurityException { PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, outLen * 8); - return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded(); + try { + return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded(); + } finally { + spec.clearPassword(); + } } /** diff --git a/lib/src/main/java/zeroecho/sdk/guard/PasswordOpener.java b/lib/src/main/java/zeroecho/sdk/guard/PasswordOpener.java index 3dabae0..ffc0520 100644 --- a/lib/src/main/java/zeroecho/sdk/guard/PasswordOpener.java +++ b/lib/src/main/java/zeroecho/sdk/guard/PasswordOpener.java @@ -36,16 +36,28 @@ package zeroecho.sdk.guard; import java.io.ByteArrayInputStream; import java.io.IOException; import java.security.GeneralSecurityException; +import java.util.Arrays; import javax.crypto.AEADBadTagException; import zeroecho.core.io.Util; +import zeroecho.sdk.Pbkdf2Limits; /** * Recipient opener for password-based entries that derives a KEK via PBKDF2 and * unwraps the CEK with AES-GCM. */ public final class PasswordOpener implements RecipientOpener { + private final Pbkdf2Limits limits; + + /** + * Creates an opener with explicit decoded-input limits. + * + * @param limits PBKDF2 safety limits + */ + public PasswordOpener(Pbkdf2Limits limits) { + this.limits = java.util.Objects.requireNonNull(limits, "limits must not be null"); + } /** * Attempts to open a password-based recipient entry using a password unlock * material. @@ -72,17 +84,34 @@ public final class PasswordOpener implements RecipientOpener { ByteArrayInputStream in = new ByteArrayInputStream(entryBlob); int iterations = Util.readPack7I(in); + try { + limits.validateDecoded(iterations); + } catch (IllegalArgumentException exception) { + throw new IOException("Invalid decoded PBKDF2 iteration count", exception); + } byte[] salt = Util.read(in, 64); byte[] wrapIv = Util.read(in, 64); byte[] wrapped = Util.read(in, 1 << 16); char[] password = ((UnlockMaterial.Password) material).password(); - byte[] kek = MultiRecipientDataSourceBuilder.pbkdf2HmacSha256(password, salt, iterations, 32); // try 256-bit + byte[] kek = null; try { - return MultiRecipientDataSourceBuilder.aesGcmUnwrap(kek, wrapIv, null, wrapped); - } catch (AEADBadTagException e256) { - kek = MultiRecipientDataSourceBuilder.pbkdf2HmacSha256(password, salt, iterations, 16); // fall back to 128-bit - return MultiRecipientDataSourceBuilder.aesGcmUnwrap(kek, wrapIv, null, wrapped); + kek = MultiRecipientDataSourceBuilder.pbkdf2HmacSha256(password, salt, iterations, 32); + try { + return MultiRecipientDataSourceBuilder.aesGcmUnwrap(kek, wrapIv, null, wrapped); + } catch (AEADBadTagException e256) { + Arrays.fill(kek, (byte) 0); + kek = MultiRecipientDataSourceBuilder.pbkdf2HmacSha256(password, salt, iterations, 16); + return MultiRecipientDataSourceBuilder.aesGcmUnwrap(kek, wrapIv, null, wrapped); + } + } finally { + Arrays.fill(password, '\0'); + Arrays.fill(salt, (byte) 0); + Arrays.fill(wrapIv, (byte) 0); + Arrays.fill(wrapped, (byte) 0); + if (kek != null) { + Arrays.fill(kek, (byte) 0); + } } } } diff --git a/lib/src/main/java/zeroecho/sdk/guard/PasswordRecipient.java b/lib/src/main/java/zeroecho/sdk/guard/PasswordRecipient.java index 2851995..dd061ef 100644 --- a/lib/src/main/java/zeroecho/sdk/guard/PasswordRecipient.java +++ b/lib/src/main/java/zeroecho/sdk/guard/PasswordRecipient.java @@ -36,34 +36,28 @@ package zeroecho.sdk.guard; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.GeneralSecurityException; +import java.util.Arrays; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.io.Util; +import zeroecho.sdk.Pbkdf2Limits; import zeroecho.sdk.util.RandomSupport; /** * Password recipient that derives a KEK via PBKDF2(HMAC-SHA-256) and wraps the * CEK with AES-GCM. */ -public final class PasswordRecipient implements Recipient { +public final class PasswordRecipient implements Recipient, Destroyable, AutoCloseable { private final char[] password; private final int iterations; private final int saltLen; private final int kekBytes; private final boolean decoy; - - /** - * Creates a password-based recipient. - * - * @param password the password; the caller should clear the array after use - * @param iterations PBKDF2 iteration count - * @param saltLen salt length in bytes - * @param kekBytes derived KEK length in bytes - * @throws NullPointerException if {@code password} is null - */ - public PasswordRecipient(char[] password, int iterations, int saltLen, int kekBytes) { - this(password, iterations, saltLen, kekBytes, false); - } + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Creates a password-based recipient that derives a key-encryption key (KEK) @@ -81,27 +75,35 @@ public final class PasswordRecipient implements Recipient { *
        *
      • The caller should clear the {@code password} array after constructing the * recipient to minimize exposure in memory.
      • - *
      • Choose an iteration count appropriate to the target platform to resist - * brute-force attacks.
      • + *
      • Choose an iteration count appropriate to the target platform to balance + * password-guessing resistance against recipient creation and opening + * latency. Counts below {@value Pbkdf2Limits#MINIMUM} are rejected.
      • *
      • Decoy recipients increase confidentiality by hiding the number of real * recipients but cannot successfully unwrap the CEK.
      • *
      * * @param password the password used as input to PBKDF2; must not be * {@code null} - * @param iterations the PBKDF2 iteration count + * @param iterations the PBKDF2 iteration count; must be at least + * {@value Pbkdf2Limits#MINIMUM} * @param saltLen length of the random salt (in bytes) - * @param kekBytes desired length of the derived KEK (in bytes) + * @param kekBytes derived KEK length; exactly 16 or 32 bytes * @param decoy {@code true} if this is a decoy recipient (fake entry that * cannot unwrap a CEK); {@code false} if it is a real * recipient - * @throws NullPointerException if {@code password} is {@code null} + * @throws NullPointerException if {@code password} is {@code null} + * @throws IllegalArgumentException if {@code iterations} is below + * {@value Pbkdf2Limits#MINIMUM} or + * {@code kekBytes} is not exactly 16 or 32 */ - public PasswordRecipient(char[] password, int iterations, int saltLen, int kekBytes, boolean decoy) { - this.password = Objects.requireNonNull(password); + /* default */ PasswordRecipient(char[] password, int iterations, int saltLen, int kekBytes, boolean decoy, + Pbkdf2Limits limits) { + int validatedKekBytes = RecipientKekSizes.requireSupported(kekBytes); + Objects.requireNonNull(limits, "limits must not be null").validateTrusted(iterations); + this.password = Objects.requireNonNull(password, "password must not be null").clone(); this.iterations = iterations; this.saltLen = saltLen; - this.kekBytes = kekBytes; + this.kekBytes = validatedKekBytes; this.decoy = decoy; } @@ -125,17 +127,29 @@ public final class PasswordRecipient implements Recipient { */ @Override public byte[] buildRecipientEntry(byte[] cek) throws GeneralSecurityException, IOException { - byte[] salt = RandomSupport.generateRandom(saltLen); - byte[] kek = MultiRecipientDataSourceBuilder.pbkdf2HmacSha256(password, salt, iterations, kekBytes); - byte[] wrapIv = RandomSupport.generateRandom(12); - byte[] wrapped = MultiRecipientDataSourceBuilder.aesGcmWrap(kek, wrapIv, null, cek); + lifecycleLock.lock(); + try { + if (destroyed) { + throw new IllegalStateException("Password recipient has been destroyed"); + } + byte[] salt = RandomSupport.generateRandom(saltLen); + byte[] kek = MultiRecipientDataSourceBuilder.pbkdf2HmacSha256(password, salt, iterations, kekBytes); + try { + byte[] wrapIv = RandomSupport.generateRandom(12); + byte[] wrapped = MultiRecipientDataSourceBuilder.aesGcmWrap(kek, wrapIv, null, cek); - ByteArrayOutputStream out = new ByteArrayOutputStream(64 + wrapped.length); - Util.writePack7I(out, iterations); - Util.write(out, salt); - Util.write(out, wrapIv); - Util.write(out, wrapped); - return out.toByteArray(); + ByteArrayOutputStream out = new ByteArrayOutputStream(64 + wrapped.length); + Util.writePack7I(out, iterations); + Util.write(out, salt); + Util.write(out, wrapIv); + Util.write(out, wrapped); + return out.toByteArray(); + } finally { + Arrays.fill(kek, (byte) 0); + } + } finally { + lifecycleLock.unlock(); + } } /** @@ -155,4 +169,37 @@ public final class PasswordRecipient implements Recipient { public boolean decoy() { return decoy; } + + /** {@inheritDoc} */ + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(password, '\0'); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + /** {@inheritDoc} */ + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + /** + * Closes this recipient by destroying its retained password copy. + */ + @Override + public void close() { + destroy(); + } } diff --git a/lib/src/main/java/zeroecho/sdk/guard/RecipientKekSizes.java b/lib/src/main/java/zeroecho/sdk/guard/RecipientKekSizes.java new file mode 100644 index 0000000..f4c671a --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/RecipientKekSizes.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.sdk.guard; + +/** + * Defines the KEK sizes supported by recipient entries without an encoded size + * discriminator. + * + *

      The current recipient format permits AES-128 and AES-256 wrapping only. + * Validation must occur before an entry is registered so every emitted entry + * remains openable by the corresponding recipient opener.

      + * + * @since 1.0 + */ +public final class RecipientKekSizes { + /** AES-128 key size in bytes. */ + public static final int AES_128_BYTES = 16; + /** AES-256 key size in bytes. */ + public static final int AES_256_BYTES = 32; + + private RecipientKekSizes() { + } + + /** + * Validates and returns a supported recipient KEK size. + * + * @param kekBytes requested KEK size in bytes + * @return {@code kekBytes} when it is exactly 16 or 32 + * @throws IllegalArgumentException if the size is not exactly 16 or 32 + */ + public static int requireSupported(int kekBytes) { + if (kekBytes != AES_128_BYTES && kekBytes != AES_256_BYTES) { + throw new IllegalArgumentException("Recipient KEK size must be exactly 16 or 32 bytes: " + kekBytes); + } + return kekBytes; + } +} diff --git a/lib/src/main/java/zeroecho/sdk/guard/RecipientOpener.java b/lib/src/main/java/zeroecho/sdk/guard/RecipientOpener.java index bce9c47..25f73e6 100644 --- a/lib/src/main/java/zeroecho/sdk/guard/RecipientOpener.java +++ b/lib/src/main/java/zeroecho/sdk/guard/RecipientOpener.java @@ -66,8 +66,11 @@ import java.security.GeneralSecurityException; * Implementation notes: *

      *
        - *
      • Openers are expected to be stateless and reusable; they must not retain - * references to {@code UnlockMaterial} or the returned CEK.
      • + *
      • Openers are reusable across every recipient entry in one envelope. + * Implementations that need cryptographic contexts must create a fresh context + * for each applicable attempt.
      • + *
      • Openers must not retain references to {@code UnlockMaterial} or the + * returned CEK.
      • *
      • Do not log sensitive values, and avoid timing-sensitive distinctions * beyond what the caller needs to continue scanning other entries.
      • *
      @@ -91,7 +94,7 @@ import java.security.GeneralSecurityException; * } * }
      */ -interface RecipientOpener { // NOPMD +public interface RecipientOpener extends AutoCloseable { // NOPMD /** * Tries to recover the content-encryption key from the supplied recipient * entry. @@ -112,4 +115,14 @@ interface RecipientOpener { // NOPMD */ byte[] tryOpen(String entryId, byte[] entryBlob, UnlockMaterial material) throws GeneralSecurityException, IOException; + + /** + * Releases opener-owned resources that were not consumed. + * + * @throws IOException if cleanup fails + */ + @Override + default void close() throws IOException { + // Stateless openers own no resources. + } } diff --git a/lib/src/main/java/zeroecho/sdk/guard/UnlockMaterial.java b/lib/src/main/java/zeroecho/sdk/guard/UnlockMaterial.java index c7264ef..cbb04fa 100644 --- a/lib/src/main/java/zeroecho/sdk/guard/UnlockMaterial.java +++ b/lib/src/main/java/zeroecho/sdk/guard/UnlockMaterial.java @@ -1,93 +1,40 @@ /******************************************************************************* * Copyright (C) 2026, Leo Galambos * All rights reserved. - * + * * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software must - * display the following acknowledgement: - * This product includes software developed by the Egothor project. - * - * 4. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * are permitted provided that the conditions in the project LICENSE are met. ******************************************************************************/ package zeroecho.sdk.guard; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; + import zeroecho.core.annotation.Describable; /** - * UnlockMaterial represents the unlocking data supplied to a recipient opener - * to recover a content-encryption key (CEK). + * Caller-owned session-operation input used to unlock a recipient entry. * - *

      Overview

      The multi-recipient envelope scans recipient entries and - * delegates each attempt to a {@code RecipientOpener}. An opener may require - * either a private key or a password to unwrap or derive the CEK. This sealed - * interface defines the two supported kinds of unlocking material and - * centralizes their lifetime and handling. - * - *

      Usage

      {@code
      - * // Decrypt with a private key
      - * DataContent decRsa = new MultiRecipientDataSourceBuilder()
      - *     .withAes(AesDataContentBuilder.builder().modeGcm(128).withHeader())
      - *     .payloadKeyBytes(32)
      - *     .unlockWith(new UnlockMaterial.Private(rsaPrivateKey))
      - *     .build(false);
      - *
      - * // Decrypt with a password
      - * char[] pwd = "correct horse battery staple".toCharArray();
      - * DataContent decPwd = new MultiRecipientDataSourceBuilder()
      - *     .withAes(AesDataContentBuilder.builder().modeCbcPkcs7().withHeader())
      - *     .payloadKeyBytes(32)
      - *     .unlockWith(new UnlockMaterial.Password(pwd))
      - *     .build(false);
      - * // Clear the password when no longer needed
      - * java.util.Arrays.fill(pwd, '\0');
      - * }
      - * - *

      Security notes

      - *
        - *
      • {@link Password} stores a reference to the caller-provided {@code char[]} - * for performance and zeroization. The caller is responsible for clearing the - * array after use.
      • - *
      • {@link Private} holds a {@link java.security.PrivateKey}. Manage the - * key's lifetime outside the opener and avoid logging or copying it - * unnecessarily.
      • - *
      + *

      Components accepting an {@code UnlockMaterial} borrow it and do not destroy + * it. The caller must keep it usable until the operation completes and destroy + * password material afterwards.

      */ -sealed public interface UnlockMaterial extends Describable { +public sealed interface UnlockMaterial extends Describable { /** - * Private holds a private key used to decrypt or decapsulate a recipient entry. + * Private key unlocking material. * - *

      - * Typical uses include RSA-OAEP decryption, ElGamal decryption, or KEM - * decapsulation with a private KEM key. - *

      - * - * @param key the private key used by a matching {@code RecipientOpener}; must - * not be null + * @param key non-null private key */ - record Private(java.security.PrivateKey key) implements UnlockMaterial, Describable { + record Private(java.security.PrivateKey key) implements UnlockMaterial { + /** Validates the key. */ + public Private { + Objects.requireNonNull(key, "key must not be null"); + } + /** {@inheritDoc} */ @Override public String description() { return "Unlock via key of " + key.getAlgorithm(); @@ -95,23 +42,84 @@ sealed public interface UnlockMaterial extends Describable { } /** - * Password holds characters used to derive a key-encryption key (KEK) for - * unwrapping the CEK. + * Destroyable password unlocking material backed by an owned character array. * - *

      - * The array reference is stored as provided to allow the caller to clear it - * after use. If defensive copying is desired, the caller should provide a copy - * and clear both copies after decryption. - *

      - * - * @param password the password characters; the caller should clear the array - * when no longer needed + *

      Construction and access use defensive copies. Destruction is idempotent + * and prevents subsequent access.

      */ - record Password(char[] password) implements UnlockMaterial, Describable { + final class Password implements UnlockMaterial, Destroyable { + private final char[] characters; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; + /** + * Creates password material from a caller-owned array. + * + * @param password source characters; retained only as a defensive copy + * @throws NullPointerException if {@code password} is {@code null} + */ + @SuppressWarnings("PMD.UseVarargs") + public Password(char[] password) { + this.characters = Objects.requireNonNull(password, "password must not be null").clone(); + } + + /** + * Returns a caller-owned password copy. + * + * @return password copy + * @throws IllegalStateException if destroyed + */ + public char[] password() { + lifecycleLock.lock(); + try { + if (destroyed) { + throw new IllegalStateException("Password material has been destroyed"); + } + return characters.clone(); + } finally { + lifecycleLock.unlock(); + } + } + + /** {@inheritDoc} */ @Override public String description() { return "Unlock via password"; } + + /** {@inheritDoc} */ + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(characters, '\0'); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + /** {@inheritDoc} */ + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + /** + * Returns a redacted diagnostic representation. + * + * @return redacted text + */ + @Override + public String toString() { + return "UnlockMaterial.Password[REDACTED]"; + } } } diff --git a/lib/src/main/java/zeroecho/sdk/guard/package-info.java b/lib/src/main/java/zeroecho/sdk/guard/package-info.java index 432267d..954d73a 100644 --- a/lib/src/main/java/zeroecho/sdk/guard/package-info.java +++ b/lib/src/main/java/zeroecho/sdk/guard/package-info.java @@ -33,7 +33,7 @@ ******************************************************************************/ /** * Multi-recipient envelope for symmetric payloads with pluggable recipients, - * stateless openers, and a compact header. + * reusable openers, and a compact header. * *

      Overview

      This package implements a streaming envelope that protects * one content-encryption key (CEK) for multiple recipients and delegates the @@ -107,9 +107,9 @@ *
    149. Separation of concerns: the envelope manages CEK * generation and recipient entries; the symmetric builder manages algorithm * parameters and payload framing.
    150. - *
    151. Stateless strategy objects: recipients encode entries; - * openers attempt recovery of the CEK; neither carries long-lived secret - * state.
    152. + *
    153. Reusable opener strategies: recipients encode entries; + * openers attempt every applicable entry and create fresh cryptographic contexts + * per attempt. Neither carries long-lived secret state.
    154. *
    155. Defensive parsing: the builder applies limits to the * number of recipients and the size of each entry blob; the symmetric stage * applies its own limits to its header and payload.
    156. @@ -122,8 +122,8 @@ * stateless; unlocking material centralizes ownership of secrets (private keys * or passwords), which simplifies secret lifecycle management and allows the * envelope to try one piece of material across heterogeneous entries in a - * uniform scan. If desired, an adapter can bind material to an opener instance - * in calling code without changing these interfaces. + * uniform scan. Custom openers receive the borrowed material for each attempt + * and must not retain it. * *

      Hardening options

      The following options are design targets and not * yet implemented in this package: @@ -139,19 +139,24 @@ * above. * *

      Typical usage

      {@code
      + * ZeroEchoSession session = new ZeroEchoSession();
      + * EncryptionContext rsaRecipient =
      + *     session.createContext("RSA", KeyUsage.ENCRYPT, rsaPub);
      + * KemContext kemRecipient =
      + *     session.createContext("ML-KEM", KeyUsage.ENCAPSULATE, kemPub);
      + *
        * // Encrypt for multiple recipients; AES-256/GCM carries its own header.
      - * DataContent enc = new MultiRecipientDataSourceBuilder()
      - *     .withAes(AesDataContentBuilder.builder().modeGcm(128).withHeader())
      + * MultiRecipientContent enc = MultiRecipientDataSourceBuilder.builder(session)
      + *     .withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader())
        *     .payloadKeyBytes(32)
      - *     .addRsaOaepRecipient(rsaPub)
      - *     .addKemRecipient("ML-KEM", kemPub, 32, 16)
      - *     .addPasswordRecipient(pass, 120_000, 16, 32)
      + *     .addRecipient(rsaRecipient)
      + *     .addRecipient(kemRecipient, 32, 16)
        *     .build(true);
        * enc.setInput(plainSource);
        *
        * // Decrypt with any one unlocking material; openers are installed by default.
      - * DataContent dec = new MultiRecipientDataSourceBuilder()
      - *     .withAes(AesDataContentBuilder.builder().modeGcm(128).withHeader())
      + * MultiRecipientContent dec = MultiRecipientDataSourceBuilder.builder(session)
      + *     .withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader())
        *     .payloadKeyBytes(32)
        *     .unlockWith(new UnlockMaterial.Private(rsaPrv))
        *     .build(false);
      diff --git a/lib/src/main/java/zeroecho/sdk/hybrid/derived/HybridDerived.java b/lib/src/main/java/zeroecho/sdk/hybrid/derived/HybridDerived.java
      index 96563b9..dfa02a4 100644
      --- a/lib/src/main/java/zeroecho/sdk/hybrid/derived/HybridDerived.java
      +++ b/lib/src/main/java/zeroecho/sdk/hybrid/derived/HybridDerived.java
      @@ -33,6 +33,7 @@
        ******************************************************************************/
       package zeroecho.sdk.hybrid.derived;
       
      +import java.util.Arrays;
       import java.util.Objects;
       
       import javax.crypto.SecretKey;
      @@ -213,9 +214,12 @@ public final class HybridDerived {
       
               int keyLenBytes = bitsToBytesStrict(keyBits);
               byte[] keyRaw = exportBytes(label + "/key", keyLenBytes); // NOPMD
      -        SecretKey key = new SecretKeySpec(keyRaw, "AES");
      -
      -        aes.withKey(key);
      +        try {
      +            SecretKey key = new SecretKeySpec(keyRaw, "AES");
      +            aes.withKey(key);
      +        } finally {
      +            Arrays.fill(keyRaw, (byte) 0);
      +        }
       
               if (ivLenBytes > 0) {
                   byte[] iv = exportBytes(label + "/iv", ivLenBytes);
      @@ -255,9 +259,12 @@ public final class HybridDerived {
       
               int keyLenBytes = bitsToBytesStrict(keyBits);
               byte[] keyRaw = exportBytes(label + "/key", keyLenBytes); // NOPMD
      -        SecretKey key = new SecretKeySpec(keyRaw, "ChaCha20");
      -
      -        chacha.withKey(key);
      +        try {
      +            SecretKey key = new SecretKeySpec(keyRaw, "ChaCha20");
      +            chacha.withKey(key);
      +        } finally {
      +            Arrays.fill(keyRaw, (byte) 0);
      +        }
       
               if (nonceLenBytes > 0) {
                   byte[] nonce = exportBytes(label + "/nonce", nonceLenBytes);
      @@ -341,10 +348,13 @@ public final class HybridDerived {
       
               int keyLenBytes = bitsToBytesStrict(keyBits);
               byte[] keyRaw = exportBytes(label + "/key", keyLenBytes);
      -
      -        // Prefer raw import to avoid duplicating MAC algorithm naming and to keep the
      -        // builder as the source of truth.
      -        return hmac.importKeyRaw(keyRaw);
      +        try {
      +            // Prefer raw import to avoid duplicating MAC algorithm naming and to keep the
      +            // builder as the source of truth.
      +            return hmac.importKeyRaw(keyRaw);
      +        } finally {
      +            Arrays.fill(keyRaw, (byte) 0);
      +        }
           }
       
           private void validateBase() {
      diff --git a/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexContext.java b/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexContext.java
      index 6496800..38cfd8f 100644
      --- a/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexContext.java
      +++ b/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexContext.java
      @@ -33,14 +33,12 @@
        ******************************************************************************/
       package zeroecho.sdk.hybrid.kex;
       
      -import java.io.ByteArrayInputStream;
      -import java.io.ByteArrayOutputStream;
      -import java.io.DataInputStream;
      -import java.io.DataOutputStream;
       import java.io.IOException;
      +import java.nio.ByteBuffer;
       import java.security.GeneralSecurityException;
       import java.security.Key;
       import java.security.PublicKey;
      +import java.util.Arrays;
       import java.util.Objects;
       import java.util.logging.Level;
       import java.util.logging.Logger;
      @@ -129,10 +127,13 @@ import zeroecho.sdk.util.Kdf;
       public final class HybridKexContext implements MessageAgreementContext {
       
           private static final Logger LOG = Logger.getLogger(HybridKexContext.class.getName());
      +    /* default */ static final int MAX_FRAME_BYTES = 65_536;
      +    private static final int FRAME_HEADER_BYTES = Integer.BYTES * 2;
       
           private final HybridKexProfile profile;
           private final AgreementContext classic;
           private final MessageAgreementContext pqc;
      +    private final SecretDeriver secretDeriver;
       
           private byte[] peerMessage;
       
      @@ -146,9 +147,15 @@ public final class HybridKexContext implements MessageAgreementContext {
            * @since 1.0
            */
           public HybridKexContext(HybridKexProfile profile, AgreementContext classic, MessageAgreementContext pqc) {
      +        this(profile, classic, pqc, Kdf::hkdfSha256);
      +    }
      +
      +    /* default */ HybridKexContext(HybridKexProfile profile, AgreementContext classic,
      +            MessageAgreementContext pqc, SecretDeriver secretDeriver) {
               this.profile = Objects.requireNonNull(profile, "profile");
               this.classic = Objects.requireNonNull(classic, "classic");
               this.pqc = Objects.requireNonNull(pqc, "pqc");
      +        this.secretDeriver = Objects.requireNonNull(secretDeriver, "secretDeriver");
           }
       
           /**
      @@ -316,21 +323,25 @@ public final class HybridKexContext implements MessageAgreementContext {
            */
           @Override
           public byte[] deriveSecret() {
      -        byte[] classicSs = classic.deriveSecret();
      -        byte[] pqcSs = pqc.deriveSecret();
      -
      -        byte[] ikm = new byte[classicSs.length + pqcSs.length];
      -        System.arraycopy(classicSs, 0, ikm, 0, classicSs.length);
      -        System.arraycopy(pqcSs, 0, ikm, classicSs.length, pqcSs.length);
      -
      +        byte[] classicSs = null;
      +        byte[] pqcSs = null;
      +        byte[] ikm = null;
               try {
      -            byte[] out = Kdf.hkdfSha256(ikm, profile.hkdfSalt(), profile.hkdfInfo(), profile.outLenBytes());
      +            classicSs = classic.deriveSecret();
      +            pqcSs = pqc.deriveSecret();
      +            int combinedLength = Math.addExact(classicSs.length, pqcSs.length);
      +            ikm = new byte[combinedLength];
      +            System.arraycopy(classicSs, 0, ikm, 0, classicSs.length);
      +            System.arraycopy(pqcSs, 0, ikm, classicSs.length, pqcSs.length);
      +            byte[] out = secretDeriver.derive(ikm, profile.hkdfSalt(), profile.hkdfInfo(), profile.outLenBytes());
                   if (LOG.isLoggable(Level.FINE)) {
                       LOG.fine("HybridKexContext.deriveSecret(): derived OKM length=" + out.length);
                   }
                   return out;
               } catch (GeneralSecurityException e) {
                   throw new IllegalStateException("HKDF-SHA256 failed", e);
      +        } catch (ArithmeticException e) {
      +            throw new IllegalStateException("Hybrid secret length exceeds supported array size", e);
               } finally {
                   zeroize(classicSs);
                   zeroize(pqcSs);
      @@ -338,6 +349,26 @@ public final class HybridKexContext implements MessageAgreementContext {
               }
           }
       
      +    /**
      +     * Package-private derivation seam used to verify cleanup on KDF failure.
      +     */
      +    /* default */
      +    @FunctionalInterface
      +    interface SecretDeriver {
      +        /**
      +         * Derives the final output from owned temporary hybrid input.
      +         *
      +         * @param ikm combined component secrets
      +         * @param salt HKDF salt
      +         * @param info HKDF context information
      +         * @param outputLength requested output length
      +         * @return derived output transferred to the caller
      +         * @throws GeneralSecurityException if derivation fails
      +         */
      +        byte[] derive(byte[] ikm, byte[] salt, byte[] info, int outputLength)
      +                throws GeneralSecurityException;
      +    }
      +
           /**
            * Closes both underlying contexts.
            *
      @@ -394,40 +425,61 @@ public final class HybridKexContext implements MessageAgreementContext {
           // Encoding helpers
           // -------------------------------------------------------------------------
       
      -    private static byte[] encode(byte[] classicMsg, byte[] pqcMsg) throws IOException {
      +    /* default */ static byte[] encode(byte[] classicMsg, byte[] pqcMsg) throws IOException {
               byte[] c = (classicMsg == null) ? new byte[0] : classicMsg;
               byte[] p = (pqcMsg == null) ? new byte[0] : pqcMsg;
      +        long frameLength = FRAME_HEADER_BYTES;
      +        frameLength += c.length;
      +        frameLength += p.length;
      +        if (frameLength > MAX_FRAME_BYTES) {
      +            throw new IOException("hybrid frame exceeds " + MAX_FRAME_BYTES + " bytes");
      +        }
       
      -        ByteArrayOutputStream bout = new ByteArrayOutputStream();
      -        DataOutputStream out = new DataOutputStream(bout);
      -
      -        out.writeInt(c.length);
      -        out.write(c);
      -
      -        out.writeInt(p.length);
      -        out.write(p);
      -
      -        out.flush();
      -        return bout.toByteArray();
      +        ByteBuffer output = ByteBuffer.allocate((int) frameLength);
      +        output.putInt(c.length);
      +        output.put(c);
      +        output.putInt(p.length);
      +        output.put(p);
      +        return output.array();
           }
       
      -    private static Parts decode(byte[] msg) throws IOException {
      -        DataInputStream in = new DataInputStream(new ByteArrayInputStream(msg));
      +    /* default */ static Parts decode(byte[] msg) throws IOException {
      +        Objects.requireNonNull(msg, "msg must not be null");
      +        if (msg.length > MAX_FRAME_BYTES) {
      +            throw new IOException("hybrid frame exceeds " + MAX_FRAME_BYTES + " bytes");
      +        }
      +        if (msg.length < FRAME_HEADER_BYTES) {
      +            throw new IOException("truncated hybrid frame header");
      +        }
       
      -        int cLen = in.readInt();
      +        int cLen = ByteBuffer.wrap(msg, 0, Integer.BYTES).getInt();
               if (cLen < 0) {
                   throw new IOException("negative classic length");
               }
      -        byte[] c = new byte[cLen];
      -        in.readFully(c);
      +        long pqcLengthOffset = Integer.BYTES;
      +        pqcLengthOffset += cLen;
      +        long payloadOffset = pqcLengthOffset + Integer.BYTES;
      +        if (payloadOffset > msg.length) {
      +            throw new IOException("truncated classic component");
      +        }
       
      -        int pLen = in.readInt();
      +        int pLen = ByteBuffer.wrap(msg, (int) pqcLengthOffset, Integer.BYTES).getInt();
               if (pLen < 0) {
                   throw new IOException("negative pqc length");
               }
      -        byte[] p = new byte[pLen];
      -        in.readFully(p);
      +        long expectedLength = payloadOffset + pLen;
      +        if (expectedLength > MAX_FRAME_BYTES) {
      +            throw new IOException("declared hybrid frame exceeds " + MAX_FRAME_BYTES + " bytes");
      +        }
      +        if (expectedLength > msg.length) {
      +            throw new IOException("truncated pqc component");
      +        }
      +        if (expectedLength < msg.length) {
      +            throw new IOException("trailing bytes in hybrid frame");
      +        }
       
      +        byte[] c = Arrays.copyOfRange(msg, Integer.BYTES, (int) pqcLengthOffset);
      +        byte[] p = Arrays.copyOfRange(msg, (int) payloadOffset, (int) expectedLength);
               return new Parts(c, p);
           }
       
      @@ -457,6 +509,6 @@ public final class HybridKexContext implements MessageAgreementContext {
               }
           }
       
      -    private record Parts(byte[] classicPart, byte[] pqcPart) {
      +    /* default */ record Parts(byte[] classicPart, byte[] pqcPart) {
           }
       }
      diff --git a/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexContexts.java b/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexContexts.java
      index 88f6ef4..d68457c 100644
      --- a/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexContexts.java
      +++ b/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexContexts.java
      @@ -33,14 +33,16 @@
        ******************************************************************************/
       package zeroecho.sdk.hybrid.kex;
       
      +import zeroecho.sdk.ZeroEchoSession;
      +
       import java.io.IOException;
       import java.security.PrivateKey;
       import java.security.PublicKey;
       import java.util.Objects;
       
      -import zeroecho.core.CryptoAlgorithms;
       import zeroecho.core.KeyUsage;
       import zeroecho.core.context.AgreementContext;
      +import zeroecho.core.context.CryptoContext;
       import zeroecho.core.context.MessageAgreementContext;
       import zeroecho.core.spec.ContextSpec;
       
      @@ -82,12 +84,9 @@ import zeroecho.core.spec.ContextSpec;
        * 

      * *

      Error handling

      - *

      - * Underlying context construction uses - * {@link CryptoAlgorithms#create(String, KeyUsage, java.security.Key, ContextSpec)} - * which may throw {@link IOException}. These factory methods propagate the - * checked exception to keep failures explicit and auditable. - *

      + *

      Context construction is in-memory and reports configuration or provider + * failures through the security exception model of {@link ZeroEchoSession}. + * I/O failures remain associated with later stream processing.

      * *

      Thread safety

      *

      @@ -123,6 +122,7 @@ public final class HybridKexContexts { * empty unless the chosen classic implementation itself supports message mode. *

      * + * @param session explicit policy and audit boundary * @param profile hybrid profile defining HKDF binding and * output length * @param classicAlgId classic agreement algorithm identifier (for @@ -142,13 +142,13 @@ public final class HybridKexContexts { * if the algorithm supports a default) * @return initiator-side {@link HybridKexContext} * @throws NullPointerException if any required argument is {@code null} - * @throws IOException if underlying context creation fails * @since 1.0 */ - public static HybridKexContext initiator(HybridKexProfile profile, String classicAlgId, + public static HybridKexContext initiator(ZeroEchoSession session, HybridKexProfile profile, String classicAlgId, PrivateKey classicInitiatorPrivate, PublicKey classicPeerPublic, ContextSpec classicSpec, String pqcAlgId, - PublicKey pqcPeerPublic, ContextSpec pqcSpec) throws IOException { + PublicKey pqcPeerPublic, ContextSpec pqcSpec) { + Objects.requireNonNull(session, "session"); Objects.requireNonNull(profile, "profile"); Objects.requireNonNull(classicAlgId, "classicAlgId"); Objects.requireNonNull(classicInitiatorPrivate, "classicInitiatorPrivate"); @@ -156,13 +156,19 @@ public final class HybridKexContexts { Objects.requireNonNull(pqcAlgId, "pqcAlgId"); Objects.requireNonNull(pqcPeerPublic, "pqcPeerPublic"); - AgreementContext classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicInitiatorPrivate, - classicSpec); - classic.setPeerPublic(classicPeerPublic); - - MessageAgreementContext pqc = CryptoAlgorithms.create(pqcAlgId, KeyUsage.AGREEMENT, pqcPeerPublic, pqcSpec); - - return new HybridKexContext(profile, classic, pqc); + AgreementContext classic = null; + MessageAgreementContext pqc = null; + try { + classic = session.createContext(classicAlgId, KeyUsage.AGREEMENT, classicInitiatorPrivate, + classicSpec); + classic.setPeerPublic(classicPeerPublic); + pqc = session.createContext(pqcAlgId, KeyUsage.AGREEMENT, pqcPeerPublic, pqcSpec); + return new HybridKexContext(profile, classic, pqc); + } catch (RuntimeException | Error failure) { // NOPMD - close partial construction + closeAfterFailure(pqc, failure); + closeAfterFailure(classic, failure); + throw failure; + } } /** @@ -185,6 +191,7 @@ public final class HybridKexContexts { * {@link HybridKexContext#setPeerMessage(byte[])}. *

      * + * @param session explicit policy and audit boundary * @param profile hybrid profile defining HKDF binding and * output length * @param classicAlgId classic agreement algorithm identifier @@ -199,13 +206,13 @@ public final class HybridKexContexts { * {@code null}) * @return responder-side {@link HybridKexContext} * @throws NullPointerException if any required argument is {@code null} - * @throws IOException if underlying context creation fails * @since 1.0 */ - public static HybridKexContext responder(HybridKexProfile profile, String classicAlgId, + public static HybridKexContext responder(ZeroEchoSession session, HybridKexProfile profile, String classicAlgId, PrivateKey classicResponderPrivate, PublicKey classicPeerPublic, ContextSpec classicSpec, String pqcAlgId, - PrivateKey pqcResponderPrivate, ContextSpec pqcSpec) throws IOException { + PrivateKey pqcResponderPrivate, ContextSpec pqcSpec) { + Objects.requireNonNull(session, "session"); Objects.requireNonNull(profile, "profile"); Objects.requireNonNull(classicAlgId, "classicAlgId"); Objects.requireNonNull(classicResponderPrivate, "classicResponderPrivate"); @@ -213,14 +220,19 @@ public final class HybridKexContexts { Objects.requireNonNull(pqcAlgId, "pqcAlgId"); Objects.requireNonNull(pqcResponderPrivate, "pqcResponderPrivate"); - AgreementContext classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicResponderPrivate, - classicSpec); - classic.setPeerPublic(classicPeerPublic); - - MessageAgreementContext pqc = CryptoAlgorithms.create(pqcAlgId, KeyUsage.AGREEMENT, pqcResponderPrivate, - pqcSpec); - - return new HybridKexContext(profile, classic, pqc); + AgreementContext classic = null; + MessageAgreementContext pqc = null; + try { + classic = session.createContext(classicAlgId, KeyUsage.AGREEMENT, classicResponderPrivate, + classicSpec); + classic.setPeerPublic(classicPeerPublic); + pqc = session.createContext(pqcAlgId, KeyUsage.AGREEMENT, pqcResponderPrivate, pqcSpec); + return new HybridKexContext(profile, classic, pqc); + } catch (RuntimeException | Error failure) { // NOPMD - close partial construction + closeAfterFailure(pqc, failure); + closeAfterFailure(classic, failure); + throw failure; + } } /** @@ -233,6 +245,7 @@ public final class HybridKexContexts { * message is the PQC encapsulation payload (typically KEM ciphertext). *

      * + * @param session explicit policy and audit boundary * @param profile hybrid profile defining HKDF binding and * output length * @param classicAlgId classic agreement algorithm identifier (e.g. @@ -248,24 +261,31 @@ public final class HybridKexContexts { * {@code null}) * @return initiator-side {@link HybridKexContext} * @throws NullPointerException if any required argument is {@code null} - * @throws IOException if underlying context creation fails */ - public static HybridKexContext initiatorPairMessage(HybridKexProfile profile, String classicAlgId, + public static HybridKexContext initiatorPairMessage(ZeroEchoSession session, HybridKexProfile profile, + String classicAlgId, zeroecho.core.alg.common.agreement.KeyPairKey classicInitiatorKeyPair, ContextSpec classicSpec, - String pqcAlgId, PublicKey pqcPeerPublic, ContextSpec pqcSpec) throws IOException { + String pqcAlgId, PublicKey pqcPeerPublic, ContextSpec pqcSpec) { + Objects.requireNonNull(session, "session"); Objects.requireNonNull(profile, "profile"); Objects.requireNonNull(classicAlgId, "classicAlgId"); Objects.requireNonNull(classicInitiatorKeyPair, "classicInitiatorKeyPair"); Objects.requireNonNull(pqcAlgId, "pqcAlgId"); Objects.requireNonNull(pqcPeerPublic, "pqcPeerPublic"); - MessageAgreementContext classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, - classicInitiatorKeyPair, classicSpec); - - MessageAgreementContext pqc = CryptoAlgorithms.create(pqcAlgId, KeyUsage.AGREEMENT, pqcPeerPublic, pqcSpec); - - return new HybridKexContext(profile, classic, pqc); + MessageAgreementContext classic = null; + MessageAgreementContext pqc = null; + try { + classic = session.createContext(classicAlgId, KeyUsage.AGREEMENT, classicInitiatorKeyPair, + classicSpec); + pqc = session.createContext(pqcAlgId, KeyUsage.AGREEMENT, pqcPeerPublic, pqcSpec); + return new HybridKexContext(profile, classic, pqc); + } catch (RuntimeException | Error failure) { // NOPMD - close partial construction + closeAfterFailure(pqc, failure); + closeAfterFailure(classic, failure); + throw failure; + } } /** @@ -278,6 +298,7 @@ public final class HybridKexContexts { * {@link HybridKexContext#deriveSecret()}. *

      * + * @param session explicit policy and audit boundary * @param profile hybrid profile defining HKDF binding and * output length * @param classicAlgId classic agreement algorithm identifier @@ -290,24 +311,41 @@ public final class HybridKexContexts { * {@code null}) * @return responder-side {@link HybridKexContext} * @throws NullPointerException if any required argument is {@code null} - * @throws IOException if underlying context creation fails */ - public static HybridKexContext responderPairMessage(HybridKexProfile profile, String classicAlgId, + public static HybridKexContext responderPairMessage(ZeroEchoSession session, HybridKexProfile profile, + String classicAlgId, zeroecho.core.alg.common.agreement.KeyPairKey classicResponderKeyPair, ContextSpec classicSpec, - String pqcAlgId, PrivateKey pqcResponderPrivate, ContextSpec pqcSpec) throws IOException { + String pqcAlgId, PrivateKey pqcResponderPrivate, ContextSpec pqcSpec) { + Objects.requireNonNull(session, "session"); Objects.requireNonNull(profile, "profile"); Objects.requireNonNull(classicAlgId, "classicAlgId"); Objects.requireNonNull(classicResponderKeyPair, "classicResponderKeyPair"); Objects.requireNonNull(pqcAlgId, "pqcAlgId"); Objects.requireNonNull(pqcResponderPrivate, "pqcResponderPrivate"); - MessageAgreementContext classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, - classicResponderKeyPair, classicSpec); + MessageAgreementContext classic = null; + MessageAgreementContext pqc = null; + try { + classic = session.createContext(classicAlgId, KeyUsage.AGREEMENT, classicResponderKeyPair, + classicSpec); + pqc = session.createContext(pqcAlgId, KeyUsage.AGREEMENT, pqcResponderPrivate, pqcSpec); + return new HybridKexContext(profile, classic, pqc); + } catch (RuntimeException | Error failure) { // NOPMD - close partial construction + closeAfterFailure(pqc, failure); + closeAfterFailure(classic, failure); + throw failure; + } + } - MessageAgreementContext pqc = CryptoAlgorithms.create(pqcAlgId, KeyUsage.AGREEMENT, pqcResponderPrivate, - pqcSpec); - - return new HybridKexContext(profile, classic, pqc); + private static void closeAfterFailure(CryptoContext context, Throwable failure) { + if (context == null) { + return; + } + try { + context.close(); + } catch (IOException | RuntimeException closeFailure) { // NOPMD - preserve close failure + failure.addSuppressed(closeFailure); + } } } diff --git a/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexExporter.java b/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexExporter.java index db74e67..825bae7 100644 --- a/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexExporter.java +++ b/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexExporter.java @@ -34,7 +34,11 @@ package zeroecho.sdk.hybrid.kex; import java.security.GeneralSecurityException; +import java.util.Arrays; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.sdk.util.Kdf; @@ -65,10 +69,12 @@ import zeroecho.sdk.util.Kdf; * * @since 1.0 */ -public final class HybridKexExporter { +public final class HybridKexExporter implements Destroyable, AutoCloseable { private final byte[] rootSecret; private final byte[] salt; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Creates an exporter seeded from a root secret. @@ -93,27 +99,36 @@ public final class HybridKexExporter { * @throws IllegalArgumentException if outLenBytes is out of range */ public byte[] export(String label, byte[] info, int outLenBytes) { - Objects.requireNonNull(label, "label"); - - if (outLenBytes < 1 || outLenBytes > 255 * 32) { - throw new IllegalArgumentException("outLenBytes must be in range 1.." + (255 * 32)); - } - - byte[] labelBytes = label.getBytes(java.nio.charset.StandardCharsets.UTF_8); - byte[] infoUse; - if (info == null || info.length == 0) { - infoUse = labelBytes; - } else { - infoUse = new byte[labelBytes.length + 1 + info.length]; - System.arraycopy(labelBytes, 0, infoUse, 0, labelBytes.length); - infoUse[labelBytes.length] = 0; - System.arraycopy(info, 0, infoUse, labelBytes.length + 1, info.length); - } - + lifecycleLock.lock(); try { - return Kdf.hkdfSha256(rootSecret, salt, infoUse, outLenBytes); - } catch (GeneralSecurityException e) { - throw new IllegalStateException("HKDF-SHA256 failed", e); + requireActive(); + Objects.requireNonNull(label, "label"); + + if (outLenBytes < 1 || outLenBytes > 255 * 32) { + throw new IllegalArgumentException("outLenBytes must be in range 1.." + (255 * 32)); + } + + byte[] labelBytes = label.getBytes(java.nio.charset.StandardCharsets.UTF_8); + byte[] infoUse; + if (info == null || info.length == 0) { + infoUse = labelBytes; + } else { + infoUse = new byte[labelBytes.length + 1 + info.length]; + System.arraycopy(labelBytes, 0, infoUse, 0, labelBytes.length); + infoUse[labelBytes.length] = 0; + System.arraycopy(info, 0, infoUse, labelBytes.length + 1, info.length); + } + + try { + return Kdf.hkdfSha256(rootSecret, salt, infoUse, outLenBytes); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("HKDF-SHA256 failed", e); + } finally { + Arrays.fill(labelBytes, (byte) 0); + Arrays.fill(infoUse, (byte) 0); + } + } finally { + lifecycleLock.unlock(); } } @@ -128,6 +143,63 @@ public final class HybridKexExporter { * @return copy of root secret */ public byte[] rootSecretCopy() { - return rootSecret.clone(); + lifecycleLock.lock(); + try { + requireActive(); + return rootSecret.clone(); + } finally { + lifecycleLock.unlock(); + } + } + + /** + * Overwrites the exporter root secret and salt. + * + *

      Destruction is idempotent. All subsequent export or diagnostic access + * fails with {@link IllegalStateException}.

      + */ + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(rootSecret, (byte) 0); + if (salt != null) { + Arrays.fill(salt, (byte) 0); + } + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + /** + * Reports whether this exporter has been destroyed. + * + * @return {@code true} after destruction + */ + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + /** + * Closes this exporter by destroying its retained secret material. + */ + @Override + public void close() { + destroy(); + } + + private void requireActive() { + if (destroyed) { + throw new IllegalStateException("Hybrid KEX exporter has been destroyed"); + } } } diff --git a/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexProfile.java b/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexProfile.java index 39e0d3f..c98a54d 100644 --- a/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexProfile.java +++ b/lib/src/main/java/zeroecho/sdk/hybrid/kex/HybridKexProfile.java @@ -78,7 +78,7 @@ public record HybridKexProfile(byte[] hkdfSalt, byte[] hkdfInfo, int outLenBytes /** * Default HKDF label used when the caller does not provide an explicit one. */ - public static final byte[] DEFAULT_INFO = "ZeroEcho-HybridKEX".getBytes(StandardCharsets.US_ASCII); + private static final byte[] DEFAULT_INFO = "ZeroEcho-HybridKEX".getBytes(StandardCharsets.US_ASCII); /** * Constructs a profile with a default HKDF info label. @@ -104,4 +104,24 @@ public record HybridKexProfile(byte[] hkdfSalt, byte[] hkdfInfo, int outLenBytes hkdfSalt = (hkdfSalt == null) ? null : hkdfSalt.clone(); hkdfInfo = (hkdfInfo == null) ? null : hkdfInfo.clone(); } + + /** + * Returns an owned copy of the optional HKDF salt. + * + * @return salt copy, or {@code null} + */ + @Override + public byte[] hkdfSalt() { + return hkdfSalt == null ? null : hkdfSalt.clone(); + } + + /** + * Returns an owned copy of the optional HKDF information. + * + * @return information copy, or {@code null} + */ + @Override + public byte[] hkdfInfo() { + return hkdfInfo == null ? null : hkdfInfo.clone(); + } } diff --git a/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureContext.java b/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureContext.java index 1ce68ef..58298ce 100644 --- a/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureContext.java +++ b/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureContext.java @@ -33,6 +33,8 @@ ******************************************************************************/ package zeroecho.sdk.hybrid.signature; +import zeroecho.sdk.ZeroEchoSession; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -88,6 +90,7 @@ import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate; */ final class HybridSignatureContext implements SignatureContext { + private final ZeroEchoSession session; private final HybridSignatureProfile profile; private final boolean produceMode; @@ -115,8 +118,10 @@ final class HybridSignatureContext implements SignatureContext { * or {@code pqcPrivate} is {@code null} * @throws IllegalArgumentException if {@code maxBufferedBytes <= 0} */ - protected HybridSignatureContext(HybridSignatureProfile profile, PrivateKey classicPrivate, PrivateKey pqcPrivate, - int maxBufferedBytes) { + /* default */ HybridSignatureContext(ZeroEchoSession session, HybridSignatureProfile profile, + PrivateKey classicPrivate, + PrivateKey pqcPrivate, int maxBufferedBytes) { + this.session = Objects.requireNonNull(session, "session"); this.profile = Objects.requireNonNull(profile, "profile"); this.classicPrivate = Objects.requireNonNull(classicPrivate, "classicPrivate"); this.pqcPrivate = Objects.requireNonNull(pqcPrivate, "pqcPrivate"); @@ -146,8 +151,10 @@ final class HybridSignatureContext implements SignatureContext { * or {@code pqcPublic} is {@code null} * @throws IllegalArgumentException if {@code maxBufferedBytes <= 0} */ - protected HybridSignatureContext(HybridSignatureProfile profile, PublicKey classicPublic, PublicKey pqcPublic, - int maxBufferedBytes) { + /* default */ HybridSignatureContext(ZeroEchoSession session, HybridSignatureProfile profile, + PublicKey classicPublic, + PublicKey pqcPublic, int maxBufferedBytes) { + this.session = Objects.requireNonNull(session, "session"); this.profile = Objects.requireNonNull(profile, "profile"); this.classicPublic = Objects.requireNonNull(classicPublic, "classicPublic"); this.pqcPublic = Objects.requireNonNull(pqcPublic, "pqcPublic"); @@ -219,17 +226,17 @@ final class HybridSignatureContext implements SignatureContext { private SignatureContext createClassic(KeyUsage usage) throws IOException { ContextSpec spec = profile.classicSpec(); if (usage == KeyUsage.SIGN) { - return CryptoAlgorithms.create(profile.classicSigId(), usage, classicPrivate, spec); + return session.createContext(profile.classicSigId(), usage, classicPrivate, spec); } - return CryptoAlgorithms.create(profile.classicSigId(), usage, classicPublic, spec); + return session.createContext(profile.classicSigId(), usage, classicPublic, spec); } private SignatureContext createPqc(KeyUsage usage) throws IOException { ContextSpec spec = profile.pqcSpec(); if (usage == KeyUsage.SIGN) { - return CryptoAlgorithms.create(profile.pqcSigId(), usage, pqcPrivate, spec); + return session.createContext(profile.pqcSigId(), usage, pqcPrivate, spec); } - return CryptoAlgorithms.create(profile.pqcSigId(), usage, pqcPublic, spec); + return session.createContext(profile.pqcSigId(), usage, pqcPublic, spec); } /** @@ -559,10 +566,10 @@ final class HybridSignatureContext implements SignatureContext { } } - private static byte[] signOne(String id, PrivateKey key, ContextSpec spec, byte[] body) throws IOException { + private byte[] signOne(String id, PrivateKey key, ContextSpec spec, byte[] body) throws IOException { final byte[][] sigHolder = new byte[1][]; - try (SignatureContext signer = CryptoAlgorithms.create(id, KeyUsage.SIGN, key, spec); + try (SignatureContext signer = session.createContext(id, KeyUsage.SIGN, key, spec); InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(body)), signer.tagLength(), 8192) { @Override @@ -584,12 +591,12 @@ final class HybridSignatureContext implements SignatureContext { } } - private static boolean verifyOne(String id, PublicKey key, ContextSpec spec, byte[] body, byte[] expected) + private boolean verifyOne(String id, PublicKey key, ContextSpec spec, byte[] body, byte[] expected) throws IOException { AtomicBoolean ok = new AtomicBoolean(false); - try (SignatureContext verifier = CryptoAlgorithms.create(id, KeyUsage.VERIFY, key, spec)) { + try (SignatureContext verifier = session.createContext(id, KeyUsage.VERIFY, key, spec)) { verifier.setVerificationApproach(new CapturePredicate(verifier.getVerificationCore(), ok)); verifier.setExpectedTag(expected); diff --git a/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureContexts.java b/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureContexts.java index 76c4e23..5930b17 100644 --- a/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureContexts.java +++ b/lib/src/main/java/zeroecho/sdk/hybrid/signature/HybridSignatureContexts.java @@ -38,6 +38,7 @@ import java.security.PublicKey; import java.util.Objects; import zeroecho.core.context.SignatureContext; +import zeroecho.sdk.ZeroEchoSession; /** * Factory for {@link SignatureContext}-compatible hybrid signature contexts. @@ -67,12 +68,14 @@ public final class HybridSignatureContexts { * @throws IllegalArgumentException if {@code maxBufferedBytes <= 0} * @since 1.0 */ - public static SignatureContext sign(HybridSignatureProfile profile, PrivateKey classicPrivate, + public static SignatureContext sign(ZeroEchoSession session, HybridSignatureProfile profile, + PrivateKey classicPrivate, PrivateKey pqcPrivate, int maxBufferedBytes) { + Objects.requireNonNull(session, "session"); Objects.requireNonNull(profile, "profile"); Objects.requireNonNull(classicPrivate, "classicPrivate"); Objects.requireNonNull(pqcPrivate, "pqcPrivate"); - return new HybridSignatureContext(profile, classicPrivate, pqcPrivate, maxBufferedBytes); + return new HybridSignatureContext(session, profile, classicPrivate, pqcPrivate, maxBufferedBytes); } /** @@ -88,11 +91,12 @@ public final class HybridSignatureContexts { * @throws IllegalArgumentException if {@code maxBufferedBytes <= 0} * @since 1.0 */ - public static SignatureContext verify(HybridSignatureProfile profile, PublicKey classicPublic, PublicKey pqcPublic, - int maxBufferedBytes) { + public static SignatureContext verify(ZeroEchoSession session, HybridSignatureProfile profile, + PublicKey classicPublic, PublicKey pqcPublic, int maxBufferedBytes) { + Objects.requireNonNull(session, "session"); Objects.requireNonNull(profile, "profile"); Objects.requireNonNull(classicPublic, "classicPublic"); Objects.requireNonNull(pqcPublic, "pqcPublic"); - return new HybridSignatureContext(profile, classicPublic, pqcPublic, maxBufferedBytes); + return new HybridSignatureContext(session, profile, classicPublic, pqcPublic, maxBufferedBytes); } } diff --git a/lib/src/main/java/zeroecho/sdk/io/SignatureTrailerInputStream.java b/lib/src/main/java/zeroecho/sdk/io/SignatureTrailerInputStream.java index ae790bb..56e751d 100644 --- a/lib/src/main/java/zeroecho/sdk/io/SignatureTrailerInputStream.java +++ b/lib/src/main/java/zeroecho/sdk/io/SignatureTrailerInputStream.java @@ -62,7 +62,7 @@ import zeroecho.core.io.TailStrippingInputStream; *

      * *

      Usage

      {@code
      - * SignatureContext sc = CryptoAlgorithms.create("Ed25519", KeyUsage.SIGN, privateKey, spec);
      + * SignatureContext sc = session.createContext("Ed25519", KeyUsage.SIGN, privateKey, spec);
        * try (InputStream in = new SignatureTrailerInputStream(
        *         sc,
        *         originalContent.getStream(),
      diff --git a/lib/src/main/java/zeroecho/sdk/package-info.java b/lib/src/main/java/zeroecho/sdk/package-info.java
      index e898735..9bed919 100644
      --- a/lib/src/main/java/zeroecho/sdk/package-info.java
      +++ b/lib/src/main/java/zeroecho/sdk/package-info.java
      @@ -32,6 +32,11 @@
        * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
        ******************************************************************************/
       /**
      - * 
      + * Consumer-facing ZeroEcho runtime and high-level integration APIs.
      + *
      + * 

      + * {@link zeroecho.sdk.ZeroEchoSession} provides immutable, explicitly scoped + * policy and audit configuration for direct cryptographic operations. + *

      */ package zeroecho.sdk; diff --git a/lib/src/main/java/zeroecho/sdk/util/Kdf.java b/lib/src/main/java/zeroecho/sdk/util/Kdf.java index 7671975..b11073c 100644 --- a/lib/src/main/java/zeroecho/sdk/util/Kdf.java +++ b/lib/src/main/java/zeroecho/sdk/util/Kdf.java @@ -34,6 +34,7 @@ package zeroecho.sdk.util; import java.security.GeneralSecurityException; +import java.util.Arrays; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; @@ -108,28 +109,43 @@ public final class Kdf { // NOPMD } final byte[] infoUse = (info == null) ? new byte[0] : info.clone(); - // ----- Extract ----- - Mac mac = Mac.getInstance(HMAC); - mac.init(new SecretKeySpec(saltUse, HMAC)); - final byte[] prk = mac.doFinal(ikm); - - // ----- Expand ----- - final int n = (length + HASH_LEN - 1) / HASH_LEN; // ceil - final byte[] okm = new byte[length]; + byte[] prk = null; byte[] t = new byte[0]; - int pos = 0; + byte[] okm = null; + boolean returned = false; + try { + Mac mac = Mac.getInstance(HMAC); + mac.init(new SecretKeySpec(saltUse, HMAC)); + prk = mac.doFinal(ikm); - for (int i = 1; i <= n; i++) { - mac.init(new SecretKeySpec(prk, HMAC)); // NOPMD - mac.update(t, 0, t.length); - mac.update(infoUse, 0, infoUse.length); - mac.update((byte) i); - t = mac.doFinal(); + final int n = (length + HASH_LEN - 1) / HASH_LEN; + okm = new byte[length]; + int pos = 0; + for (int i = 1; i <= n; i++) { + mac.init(new SecretKeySpec(prk, HMAC)); // NOPMD + mac.update(t, 0, t.length); + mac.update(infoUse, 0, infoUse.length); + mac.update((byte) i); + byte[] next = mac.doFinal(); + Arrays.fill(t, (byte) 0); + t = next; - final int toCopy = Math.min(HASH_LEN, length - pos); - System.arraycopy(t, 0, okm, pos, toCopy); - pos += toCopy; + final int toCopy = Math.min(HASH_LEN, length - pos); + System.arraycopy(t, 0, okm, pos, toCopy); + pos += toCopy; + } + returned = true; + return okm; + } finally { + Arrays.fill(saltUse, (byte) 0); + Arrays.fill(infoUse, (byte) 0); + if (prk != null) { + Arrays.fill(prk, (byte) 0); + } + Arrays.fill(t, (byte) 0); + if (!returned && okm != null) { + Arrays.fill(okm, (byte) 0); + } } - return okm; } } diff --git a/lib/src/main/java/zeroecho/sdk/util/Password.java b/lib/src/main/java/zeroecho/sdk/util/Password.java index 8707eda..3ab6479 100644 --- a/lib/src/main/java/zeroecho/sdk/util/Password.java +++ b/lib/src/main/java/zeroecho/sdk/util/Password.java @@ -33,11 +33,6 @@ ******************************************************************************/ package zeroecho.sdk.util; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.concurrent.locks.ReentrantLock; /** * Utility class for generating random passwords and secure random byte arrays. @@ -50,11 +45,8 @@ import java.util.concurrent.locks.ReentrantLock; * bytes. * *

      - * A single instance of {@link SecureRandom} is used unless - * {@code UNSAFE_SINGLE_SECURE} is set to {@code false}, in which case a new - * {@link SecureRandom} instance is created for each operation. - *

      - * This class is thread-safe through use of a {@link ReentrantLock}. + * Randomness is supplied by the shared thread-safe source in + * {@link RandomSupport}. * * @author Leo Galambos */ @@ -67,80 +59,33 @@ public final class Password { } /** - * Generates a random password by filling the provided byte array with - * cryptographically strong random bytes. The randomness is influenced by a - * combination of a user-supplied seed string and a randomly generated salt, - * ensuring that the result is both secure and non-deterministic across multiple - * invocations with the same input. - *

      - * Internally, the method uses the SHA-256 digest of the concatenation of the - * seed and a 16-byte random salt to derive a seed for a {@link SecureRandom} - * instance. This seed is mixed into the internal state of the - * {@code SecureRandom} generator to produce a high-entropy, unpredictable byte - * sequence. As a result, the output differs each time the method is called, - * even with the same seed and password buffer. - *

      - * Note: The generated salt is not returned or stored. If you require - * reproducible output or need to verify the result later, you must persist the - * salt separately. + * Fills the supplied array with cryptographically strong random bytes. * - * @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} + * @param password array to fill; must not be {@code null} + * @return {@code password} + * @throws NullPointerException if {@code password} 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; + public static byte[] generateRandom(final byte[] password) { + return RandomSupport.generateRandom(password); } /** - * Generates a cryptographically secure random password consisting of printable - * ASCII characters. + * Generates a printable password in a caller-owned character array. * - * @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 + * @param length desired password length; must be positive + * @return a newly allocated printable password + * @throws IllegalArgumentException if {@code length} is not positive */ - public static String generatePrintablePassword(final int length) { + public static char[] generatePrintablePasswordChars(final int length) { if (length <= 0) { throw new IllegalArgumentException("Password length must be greater than zero"); } - final StringBuilder password = new StringBuilder(length); + final char[] password = new char[length]; for (int i = 0; i < length; i++) { - final int ascii = RandomSupport.getRandom().nextInt('~' - '!' + 1) + '!'; - password.append((char) ascii); + password[i] = (char) (RandomSupport.nextInt('~' - '!' + 1) + '!'); } - - return password.toString(); + return password; } + } diff --git a/lib/src/main/java/zeroecho/sdk/util/RandomSupport.java b/lib/src/main/java/zeroecho/sdk/util/RandomSupport.java index 3c88e29..a41955b 100644 --- a/lib/src/main/java/zeroecho/sdk/util/RandomSupport.java +++ b/lib/src/main/java/zeroecho/sdk/util/RandomSupport.java @@ -35,25 +35,15 @@ package zeroecho.sdk.util; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; /** * Utility class providing support for secure random number generation. *

      - * This class encapsulates logic for generating cryptographically secure random - * data using Java's {@link SecureRandom}. It optionally supports a singleton - * instance pattern for the {@code SecureRandom} generator, controlled by the - * {@code UNSAFE_SINGLE_SECURE} flag. - *

      - * - *

      - * Note: Reusing a single {@code SecureRandom} instance can - * improve performance, but may reduce randomness guarantees in some - * multi-threaded or long-lived contexts. Always assess your threat model and - * performance needs when toggling {@code UNSAFE_SINGLE_SECURE}. + * This class provides cryptographically secure random data through one + * process-wide, thread-safe {@link SecureRandom}. It selects the platform's + * strong implementation when available and otherwise uses the platform default. *

      * * @author Leo Galambos @@ -61,45 +51,28 @@ import java.util.logging.Logger; 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 static final SecureRandom RANDOM = createRandom(); private RandomSupport() { // this is a utility class } /** - * Retrieves a {@link SecureRandom} instance. - *

      - * If {@code UNSAFE_SINGLE_SECURE} is true, returns a shared instance; - * otherwise, creates a new {@link SecureRandom} instance. + * Returns the shared {@link SecureRandom} instance. * - * @return A {@link SecureRandom} instance for random number generation. + * @return the shared random source */ public static SecureRandom getRandom() { - instanceLock.lock(); + return RANDOM; + } + + private static SecureRandom createRandom() { try { - if (UNSAFE_SINGLE_SECURE) { - return RANDOM; - } else { - LOG.log(Level.INFO, "creating a new SecureRandom"); - return new SecureRandom(); - } - } finally { - instanceLock.unlock(); + return SecureRandom.getInstanceStrong(); + } catch (NoSuchAlgorithmException exception) { + LOG.log(Level.WARNING, "Strong SecureRandom unavailable; using the platform default"); + return new SecureRandom(); } } @@ -124,7 +97,19 @@ public final class RandomSupport { * @throws NullPointerException if {@code buffer} is {@code null} */ public static byte[] generateRandom(final byte[] buffer) { - getRandom().nextBytes(buffer); + RANDOM.nextBytes(buffer); return buffer; } + + /** + * Returns a uniformly distributed value between zero (inclusive) and the + * specified bound (exclusive). + * + * @param bound exclusive upper bound; must be positive + * @return a uniformly distributed value + * @throws IllegalArgumentException if {@code bound} is not positive + */ + public static int nextInt(final int bound) { + return RANDOM.nextInt(bound); + } } diff --git a/lib/src/test/java/zeroecho/core/CapabilityValueSemanticsTest.java b/lib/src/test/java/zeroecho/core/CapabilityValueSemanticsTest.java new file mode 100644 index 0000000..05bedfb --- /dev/null +++ b/lib/src/test/java/zeroecho/core/CapabilityValueSemanticsTest.java @@ -0,0 +1,128 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.context.DigestContext; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.spec.ContextSpec; + +/** + * Verifies stable value semantics for capability metadata defaults. + */ +class CapabilityValueSemanticsTest { + + @Test + void semanticallyEqualCapabilitiesHaveEqualHashes() { + System.out.println("semanticallyEqualCapabilitiesHaveEqualHashes"); + Capability first = capability(() -> new TestSpec("SHA-256")); + Capability second = capability(() -> new TestSpec("SHA-256")); + + assertEquals(first, second); + assertEquals(first.hashCode(), second.hashCode()); + + System.out.println("...hash=" + first.hashCode()); + System.out.println("semanticallyEqualCapabilitiesHaveEqualHashes...ok"); + } + + @Test + void defaultSupplierIsResolvedOnceAndRepeatedAccessIsStable() { + System.out.println("defaultSupplierIsResolvedOnceAndRepeatedAccessIsStable"); + AtomicInteger evaluations = new AtomicInteger(); + Capability capability = capability(() -> { + evaluations.incrementAndGet(); + return new TestSpec("SHA-256"); + }); + + ContextSpec first = capability.defaultSpec(); + ContextSpec second = capability.defaultSpec(); + assertEquals(1, evaluations.get()); + assertSame(first, second); + + System.out.println("...evaluations=" + evaluations.get()); + System.out.println("defaultSupplierIsResolvedOnceAndRepeatedAccessIsStable...ok"); + } + + @Test + void semanticallyDifferentCapabilitiesAreNotEqual() { + System.out.println("semanticallyDifferentCapabilitiesAreNotEqual"); + Capability first = capability(() -> new TestSpec("SHA-256")); + Capability second = capability(() -> new TestSpec("SHA-512")); + + assertNotEquals(first, second); + + System.out.println("...firstDefault=" + first.defaultSpec()); + System.out.println("...secondDefault=" + second.defaultSpec()); + System.out.println("semanticallyDifferentCapabilitiesAreNotEqual...ok"); + } + + @Test + void nullAndIncompatibleDefaultsAreRejectedAtConstruction() { + System.out.println("nullAndIncompatibleDefaultsAreRejectedAtConstruction"); + assertThrows(NullPointerException.class, () -> capability(() -> null)); + assertThrows(IllegalArgumentException.class, + () -> new Capability("DIGEST", AlgorithmFamily.DIGEST, KeyUsage.DIGEST, DigestContext.class, + NullKey.class, TestSpec.class, new OtherSpec())); + + System.out.println("...invalidDefaultsRejected=true"); + System.out.println("nullAndIncompatibleDefaultsAreRejectedAtConstruction...ok"); + } + + @Test + void metadataMemoizationDoesNotChangeRuntimeSupplierLifecycle() throws IOException { + System.out.println("metadataMemoizationDoesNotChangeRuntimeSupplierLifecycle"); + AtomicInteger evaluations = new AtomicInteger(); + List runtimeSpecs = new ArrayList<>(); + TestAlgorithm algorithm = new TestAlgorithm(evaluations, runtimeSpecs); + Capability capability = algorithm.listCapabilities().get(0); + + ContextSpec metadataFirst = capability.defaultSpec(); + ContextSpec metadataSecond = capability.defaultSpec(); + algorithm.createContext(KeyUsage.DIGEST, NullKey.INSTANCE, null); + algorithm.createContext(KeyUsage.DIGEST, NullKey.INSTANCE, null); + + assertSame(metadataFirst, metadataSecond); + assertEquals(3, evaluations.get()); + assertNotEquals(runtimeSpecs.get(0), runtimeSpecs.get(1)); + System.out.println("...evaluations=" + evaluations.get()); + System.out.println("...runtimeDefaults=" + runtimeSpecs.size()); + System.out.println("metadataMemoizationDoesNotChangeRuntimeSupplierLifecycle...ok"); + } + + private static Capability capability(java.util.function.Supplier defaultSpec) { + return new Capability("DIGEST", AlgorithmFamily.DIGEST, KeyUsage.DIGEST, DigestContext.class, NullKey.class, + TestSpec.class, defaultSpec.get()); + } + + private record TestSpec(String name) implements ContextSpec { + } + + private static final class OtherSpec implements ContextSpec { + } + + private static final class TestAlgorithm extends AbstractCryptoAlgorithm { + private TestAlgorithm(AtomicInteger evaluations, List runtimeSpecs) { + super("TEST", "Test", "test"); + capability(AlgorithmFamily.DIGEST, KeyUsage.DIGEST, DigestContext.class, NullKey.class, TestSpec.class, + (key, spec) -> { + runtimeSpecs.add(spec); + return mock(DigestContext.class); + }, + () -> new TestSpec(Integer.toString(evaluations.incrementAndGet()))); + } + } +} diff --git a/lib/src/test/java/zeroecho/core/CatalogContractTest.java b/lib/src/test/java/zeroecho/core/CatalogContractTest.java index 19787b3..4bddaa0 100644 --- a/lib/src/test/java/zeroecho/core/CatalogContractTest.java +++ b/lib/src/test/java/zeroecho/core/CatalogContractTest.java @@ -71,11 +71,16 @@ import zeroecho.core.context.MacContext; import zeroecho.core.context.SignatureContext; import zeroecho.core.io.TailStrippingInputStream; import zeroecho.core.spec.AlgorithmKeySpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; -import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.core.KeyOperation; +import zeroecho.core.KeyOperationInfo; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.SymmetricKeyGenerator; +import zeroecho.core.audit.AuditMode; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.sdk.util.BouncyCastleActivator; public class CatalogContractTest { + private static ZeroEchoSession session; static Level effectiveLevel(Logger lg) { for (Logger x = lg; x != null; x = x.getParent()) { @@ -102,9 +107,9 @@ public class CatalogContractTest { Logger jul = Logger.getLogger("zeroecho.audit"); jul.setLevel(Level.FINE); // see PROGRESS at FINE - CryptoAlgorithms.setAuditListener(JulAuditListenerStd.builder().logger(jul).infoLevel(Level.INFO) - .warnLevel(Level.WARNING).progressLevel(Level.FINE).includeStackTraces(true).build()); - CryptoAlgorithms.setAuditMode(CryptoAlgorithms.AuditMode.WRAP); + session = new ZeroEchoSession().withAuditListener(JulAuditListenerStd.builder().logger(jul) + .infoLevel(Level.INFO).warnLevel(Level.WARNING).progressLevel(Level.FINE) + .includeStackTraces(true).build()).withAuditMode(AuditMode.WRAP); dump(""); dump("zeroecho.core.audit"); @@ -167,21 +172,21 @@ public class CatalogContractTest { void genericRoundTrips() throws Exception { logBegin(); byte[] msg = "roundtrip".getBytes(); - CryptoAlgorithms.setAuditMode(CryptoAlgorithms.AuditMode.WRAP); - for (String id : CryptoAlgorithms.available()) { CryptoAlgorithm alg = CryptoAlgorithms.require(id); + boolean hasAsym = alg.keyOperations().stream() + .anyMatch(info -> info.operation() == KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE); + boolean hasSym = alg.keyOperations().stream() + .anyMatch(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE); // SIGN/VERIFY (asymmetric) if (alg.roles().contains(KeyUsage.SIGN) && alg.roles().contains(KeyUsage.VERIFY) - && !alg.asymmetricBuildersInfo().isEmpty()) { + && hasAsym) { trySignVerify(id, msg); System.out.println(); } // ENCRYPT/DECRYPT - boolean hasSym = !alg.symmetricBuildersInfo().isEmpty(); - boolean hasAsym = !alg.asymmetricBuildersInfo().isEmpty(); if (alg.roles().contains(KeyUsage.ENCRYPT) && alg.roles().contains(KeyUsage.DECRYPT)) { if (hasSym || hasAsym) { tryEncryptDecrypt(id, msg, hasSym, hasAsym); @@ -191,7 +196,7 @@ public class CatalogContractTest { // KEM if (alg.roles().contains(KeyUsage.ENCAPSULATE) && alg.roles().contains(KeyUsage.DECAPSULATE) - && !alg.asymmetricBuildersInfo().isEmpty()) { + && hasAsym) { tryKem(id, msg); System.out.println(); } @@ -203,7 +208,7 @@ public class CatalogContractTest { } // MAC (single role now; verification via setExpectedTag) - if (alg.roles().contains(KeyUsage.MAC) && !alg.symmetricBuildersInfo().isEmpty()) { + if (alg.roles().contains(KeyUsage.MAC) && hasSym) { tryMac(id, msg); System.out.println(); } @@ -222,7 +227,7 @@ public class CatalogContractTest { } // SIGN: produce [body][signature] and capture trailer - SignatureContext signer = CryptoAlgorithms.create(id, KeyUsage.SIGN, kp.getPrivate(), null); + SignatureContext signer = session.createContext(id, KeyUsage.SIGN, kp.getPrivate(), null); final byte[][] sigHolder = new byte[1][]; final int sigLen = signer.tagLength(); try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(msg)), sigLen, 8192) { @@ -240,7 +245,7 @@ public class CatalogContractTest { assertTrue(sig.length > 0, "signature empty"); // VERIFY: supply signature via setExpectedTag and drain (throws on mismatch) - SignatureContext verifier = CryptoAlgorithms.create(id, KeyUsage.VERIFY, kp.getPublic(), null); + SignatureContext verifier = session.createContext(id, KeyUsage.VERIFY, kp.getPublic(), null); verifier.setExpectedTag(sig); try (InputStream verIn = verifier.wrap(new ByteArrayInputStream(msg))) { readAll(verIn); @@ -261,14 +266,14 @@ public class CatalogContractTest { if (sk != null) { conflux.CtxInterface session = conflux.Ctx.INSTANCE.getContext("encdec-" + System.nanoTime()); - EncryptionContext enc = CryptoAlgorithms.create(id, KeyUsage.ENCRYPT, sk, null); + EncryptionContext enc = CatalogContractTest.session.createContext(id, KeyUsage.ENCRYPT, sk, null); if (enc instanceof zeroecho.core.spi.ContextAware ca) { ca.setContext(session); } byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); enc.close(); - EncryptionContext dec = CryptoAlgorithms.create(id, KeyUsage.DECRYPT, sk, null); + EncryptionContext dec = CatalogContractTest.session.createContext(id, KeyUsage.DECRYPT, sk, null); if (dec instanceof zeroecho.core.spi.ContextAware ca2) { ca2.setContext(session); } @@ -284,11 +289,11 @@ public class CatalogContractTest { if (hasAsym) { KeyPair kp = tryKeyPairWithDefaultSpec(alg); if (kp != null) { - EncryptionContext enc = CryptoAlgorithms.create(id, KeyUsage.ENCRYPT, kp.getPublic(), null); + EncryptionContext enc = session.createContext(id, KeyUsage.ENCRYPT, kp.getPublic(), null); byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); enc.close(); - EncryptionContext dec = CryptoAlgorithms.create(id, KeyUsage.DECRYPT, kp.getPrivate(), null); + EncryptionContext dec = session.createContext(id, KeyUsage.DECRYPT, kp.getPrivate(), null); byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); dec.close(); @@ -308,8 +313,8 @@ public class CatalogContractTest { return; } - KemContext pub = CryptoAlgorithms.create(id, KeyUsage.ENCAPSULATE, kp.getPublic(), null); - KemContext prv = CryptoAlgorithms.create(id, KeyUsage.DECAPSULATE, kp.getPrivate(), null); + KemContext pub = session.createContext(id, KeyUsage.ENCAPSULATE, kp.getPublic(), null); + KemContext prv = session.createContext(id, KeyUsage.DECAPSULATE, kp.getPrivate(), null); KemContext.KemResult res = pub.encapsulate(); byte[] ss = prv.decapsulate(res.ciphertext()); @@ -326,7 +331,7 @@ public class CatalogContractTest { private void tryDigest(String id, byte[] msg) throws Exception { logBegin(id, Integer.valueOf(msg.length)); - DigestContext dctx = CryptoAlgorithms.create(id, KeyUsage.DIGEST, NullKey.INSTANCE, null); + DigestContext dctx = session.createContext(id, KeyUsage.DIGEST, NullKey.INSTANCE, null); final byte[][] digestHolder = new byte[1][]; final int tagLen = dctx.tagLength(); @@ -360,7 +365,7 @@ public class CatalogContractTest { } // Produce tag: [body][tag], capture trailer - MacContext mac = CryptoAlgorithms.create(id, KeyUsage.MAC, sk, null); + MacContext mac = session.createContext(id, KeyUsage.MAC, sk, null); final byte[][] tagHolder = new byte[1][]; final int tagLen = mac.tagLength(); try (InputStream in = new TailStrippingInputStream(mac.wrap(new ByteArrayInputStream(msg)), tagLen, 8192) { @@ -378,7 +383,7 @@ public class CatalogContractTest { assertTrue(tag.length > 0); // Verify: provide expected tag and drain (throws on mismatch) - MacContext ver = CryptoAlgorithms.create(id, KeyUsage.MAC, sk, null); + MacContext ver = session.createContext(id, KeyUsage.MAC, sk, null); ver.setExpectedTag(tag); try (InputStream in = ver.wrap(new ByteArrayInputStream(msg))) { readAll(in); @@ -406,33 +411,32 @@ public class CatalogContractTest { private KeyPair tryKeyPairWithDefaultSpec(CryptoAlgorithm alg) { logBegin(alg.id()); try { - List infos = alg.asymmetricBuildersInfo(); + List infos = alg.keyOperations().stream() + .filter(info -> info.operation() == KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE).toList(); if (infos.isEmpty()) { System.out.println("no asymmetric builder info"); logEnd(); return null; } - for (CryptoAlgorithm.AsymBuilderInfo bi : infos) { - if (bi.defaultKeySpec == null) { + for (KeyOperationInfo bi : infos) { + if (bi.defaultSpec() == null) { continue; } try { @SuppressWarnings("unchecked") - Class specType = (Class) bi.specType; - AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec; + Class specType = (Class) bi.specType(); + AlgorithmKeySpec spec = bi.defaultSpec(); - AsymmetricKeyBuilder builder = alg.asymmetricKeyBuilder(specType); + AsymmetricKeyPairGenerator builder = alg.asymmetricKeyPairGenerator(specType); System.out.println("...building with " + specType.getName()); KeyPair kp = builder.generateKeyPair(spec); if (kp != null) { logEnd(); return kp; } - } catch (UnsupportedOperationException e) { - // import-only } catch (Throwable t) { - System.out.println("builder " + bi.specType.getSimpleName() + " failed to generate keypair: " + System.out.println("builder " + bi.specType().getSimpleName() + " failed to generate keypair: " + t.getClass().getSimpleName() + ": " + t.getMessage()); } } @@ -449,30 +453,29 @@ public class CatalogContractTest { private SecretKey tryGenerateSecretWithDefaultSpec(CryptoAlgorithm alg) { logBegin(alg.id()); try { - List infos = alg.symmetricBuildersInfo(); + List infos = alg.keyOperations().stream() + .filter(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE).toList(); if (infos.isEmpty()) { System.out.println("no symmetric builder info"); logEnd(); return null; } - for (CryptoAlgorithm.SymBuilderInfo bi : infos) { - if (bi.defaultKeySpec() == null) { + for (KeyOperationInfo bi : infos) { + if (bi.defaultSpec() == null) { continue; } try { @SuppressWarnings("unchecked") Class specType = (Class) bi.specType(); - AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec(); + AlgorithmKeySpec spec = bi.defaultSpec(); - SymmetricKeyBuilder builder = alg.symmetricKeyBuilder(specType); + SymmetricKeyGenerator builder = alg.symmetricKeyGenerator(specType); SecretKey sk = builder.generateSecret(spec); if (sk != null) { logEnd(); return sk; } - } catch (UnsupportedOperationException e) { - // import-only } catch (Throwable t) { System.out.println("symmetric builder " + bi.specType().getSimpleName() + " failed to generate secret: " + t.getClass().getSimpleName() + ": " + t.getMessage()); diff --git a/lib/src/test/java/zeroecho/core/CryptoAlgorithmsAuditWrapTest.java b/lib/src/test/java/zeroecho/core/CryptoAlgorithmsAuditWrapTest.java index dcde6e0..3c75c4a 100644 --- a/lib/src/test/java/zeroecho/core/CryptoAlgorithmsAuditWrapTest.java +++ b/lib/src/test/java/zeroecho/core/CryptoAlgorithmsAuditWrapTest.java @@ -34,20 +34,41 @@ ******************************************************************************/ package zeroecho.core; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.Proxy; +import java.security.Key; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import zeroecho.core.CryptoAlgorithms.AuditMode; import zeroecho.core.audit.AuditListener; +import zeroecho.core.audit.AuditedContexts; import zeroecho.core.context.AgreementContext; import zeroecho.core.context.CryptoContext; import zeroecho.core.context.DigestContext; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.KemContext.KemResult; +import zeroecho.core.context.MacContext; +import zeroecho.core.context.MessageAgreementContext; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.spec.ContextSpec; +import zeroecho.core.tag.TagEngine; /** * Verifies audited proxy wrapping for representative {@link CryptoContext} @@ -55,7 +76,7 @@ import zeroecho.core.context.DigestContext; * *

      * These tests focus on the internal audit wrapping path used by - * {@link CryptoAlgorithms#wrapForAudit(CryptoContext, zeroecho.core.audit.AuditListener, KeyUsage)}. + * {@link AuditedContexts#wrap(CryptoContext, AuditListener, KeyUsage)}. * They verify that representative context types are wrapped as audited JDK * proxies and that the resulting wrapper preserves the expected basic * delegation behavior. @@ -63,18 +84,12 @@ import zeroecho.core.context.DigestContext; */ class CryptoAlgorithmsAuditWrapTest { - @AfterEach - void restoreAuditConfiguration() { - CryptoAlgorithms.setAuditListener(AuditListener.noop()); - CryptoAlgorithms.setAuditMode(AuditMode.OFF); - } - @Test void wrapForAuditDigestContextReturnsProxy() { System.out.println("wrapForAuditDigestContextReturnsProxy"); DigestContext context = mock(DigestContext.class); - DigestContext wrapped = CryptoAlgorithms.wrapForAudit(context, AuditListener.noop(), KeyUsage.DIGEST); + DigestContext wrapped = (DigestContext) AuditedContexts.wrap(context, AuditListener.noop(), KeyUsage.DIGEST); System.out.println("...ctxClass=" + wrapped.getClass().getName()); assertTrue(Proxy.isProxyClass(wrapped.getClass()), "Digest context should be wrapped as JDK proxy"); @@ -87,7 +102,8 @@ class CryptoAlgorithmsAuditWrapTest { System.out.println("wrapForAuditAgreementContextReturnsProxy"); AgreementContext context = mock(AgreementContext.class); - AgreementContext wrapped = CryptoAlgorithms.wrapForAudit(context, AuditListener.noop(), KeyUsage.AGREEMENT); + AgreementContext wrapped = (AgreementContext) AuditedContexts.wrap(context, AuditListener.noop(), + KeyUsage.AGREEMENT); System.out.println("...ctxClass=" + wrapped.getClass().getName()); assertTrue(Proxy.isProxyClass(wrapped.getClass()), "Agreement context should be wrapped as JDK proxy"); @@ -100,7 +116,7 @@ class CryptoAlgorithmsAuditWrapTest { System.out.println("wrapForAuditDigestContextCloseDelegatesToWrappedContext"); DigestContext context = mock(DigestContext.class); - DigestContext wrapped = CryptoAlgorithms.wrapForAudit(context, AuditListener.noop(), KeyUsage.DIGEST); + DigestContext wrapped = (DigestContext) AuditedContexts.wrap(context, AuditListener.noop(), KeyUsage.DIGEST); wrapped.close(); @@ -108,4 +124,182 @@ class CryptoAlgorithmsAuditWrapTest { System.out.println("...wrappedCloseDelegated=true"); System.out.println("wrapForAuditDigestContextCloseDelegatesToWrappedContext...ok"); } -} \ No newline at end of file + + @Test + void wrapAllFamilies() throws Exception { + System.out.println("wrapAllFamilies"); + byte[] body = { 1, 2, 3 }; + + RecordingListener encryptionEvents = new RecordingListener(); + EncryptionContext encryption = mock(EncryptionContext.class); + when(encryption.attach(any(InputStream.class))).thenAnswer(invocation -> invocation.getArgument(0)); + EncryptionContext auditedEncryption = wrap(encryption, encryptionEvents, KeyUsage.ENCRYPT); + assertArrayEquals(body, auditedEncryption.attach(new ByteArrayInputStream(body)).readAllBytes()); + encryptionEvents.assertCreationBefore("progress"); + + assertTagFamily(mockTag(SignatureContext.class), KeyUsage.SIGN, body); + assertTagFamily(mockTag(MacContext.class), KeyUsage.MAC, body); + assertTagFamily(mockTag(DigestContext.class), KeyUsage.DIGEST, body); + + RecordingListener agreementEvents = new RecordingListener(); + AgreementContext agreement = mock(AgreementContext.class); + PublicKey peer = mock(PublicKey.class); + when(agreement.deriveSecret()).thenReturn(new byte[] { 4, 5 }); + AgreementContext auditedAgreement = wrap(agreement, agreementEvents, KeyUsage.AGREEMENT); + auditedAgreement.setPeerPublic(peer); + assertArrayEquals(new byte[] { 4, 5 }, auditedAgreement.deriveSecret()); + agreementEvents.assertCreationBefore("peer"); + agreementEvents.assertBefore("peer", "derived"); + + RecordingListener messageEvents = new RecordingListener(); + MessageAgreementContext messageAgreement = mock(MessageAgreementContext.class); + when(messageAgreement.getPeerMessage()).thenReturn(new byte[] { 6, 7, 8 }); + when(messageAgreement.deriveSecret()).thenReturn(new byte[] { 9 }); + MessageAgreementContext auditedMessage = wrap(messageAgreement, messageEvents, KeyUsage.AGREEMENT); + auditedMessage.setPeerMessage(new byte[] { 6 }); + assertArrayEquals(new byte[] { 6, 7, 8 }, auditedMessage.getPeerMessage()); + assertArrayEquals(new byte[] { 9 }, auditedMessage.deriveSecret()); + messageEvents.assertCreationBefore("message-set"); + messageEvents.assertBefore("message-set", "message-get"); + messageEvents.assertBefore("message-get", "derived"); + + RecordingListener kemEvents = new RecordingListener(); + KemContext kem = mock(KemContext.class); + KemResult result = new KemResult(new byte[] { 10, 11 }, new byte[] { 12 }); + when(kem.encapsulate()).thenReturn(result); + when(kem.decapsulate(any(byte[].class))).thenReturn(new byte[] { 12 }); + KemContext auditedKem = wrap(kem, kemEvents, KeyUsage.ENCAPSULATE); + assertSame(result, auditedKem.encapsulate()); + assertArrayEquals(new byte[] { 12 }, auditedKem.decapsulate(new byte[] { 10, 11 })); + kemEvents.assertCreationBefore("encapsulated"); + kemEvents.assertBefore("encapsulated", "decapsulated"); + + System.out.println("...families=7"); + System.out.println("wrapAllFamilies...ok"); + } + + @Test + void wrapFailureAndIdempotency() throws Exception { + System.out.println("wrapFailureAndIdempotency"); + RecordingListener listener = new RecordingListener(); + IOException expected = new IOException("controlled read failure"); + EncryptionContext target = mock(EncryptionContext.class); + when(target.attach(any(InputStream.class))).thenReturn(new InputStream() { + @Override + public int read() throws IOException { + throw expected; + } + }); + + EncryptionContext wrapped = wrap(target, listener, KeyUsage.DECRYPT); + int eventsAfterCreation = listener.events.size(); + assertSame(wrapped, AuditedContexts.wrap(wrapped, listener, KeyUsage.DECRYPT)); + assertEquals(eventsAfterCreation, listener.events.size()); + + IOException actual = assertThrows(IOException.class, + () -> wrapped.attach(new ByteArrayInputStream(new byte[0])).readAllBytes()); + assertSame(expected, actual); + assertEquals(1, listener.count("failure")); + listener.assertCreationBefore("failure"); + + System.out.println("...failureEvents=" + listener.count("failure")); + System.out.println("wrapFailureAndIdempotency...ok"); + } + + private static T wrap(T context, RecordingListener listener, KeyUsage usage) { + @SuppressWarnings("unchecked") + T wrapped = (T) AuditedContexts.wrap(context, listener, usage); + assertTrue(Proxy.isProxyClass(wrapped.getClass())); + return wrapped; + } + + private static > T mockTag(Class type) throws IOException { + T context = mock(type); + when(context.tagLength()).thenReturn(2); + when(context.wrap(any(InputStream.class))).thenAnswer(invocation -> { + InputStream upstream = invocation.getArgument(0); + return new java.io.SequenceInputStream(upstream, new ByteArrayInputStream(new byte[] { 0, 0 })); + }); + return context; + } + + private static void assertTagFamily(CryptoContext context, KeyUsage usage, byte[] body) throws IOException { + RecordingListener listener = new RecordingListener(); + CryptoContext wrapped = wrap(context, listener, usage); + TagEngine engine = (TagEngine) wrapped; + byte[] output = engine.wrap(new ByteArrayInputStream(body)).readAllBytes(); + + assertArrayEquals(new byte[] { 1, 2, 3, 0, 0 }, output); + assertEquals(1, listener.count("tag")); + listener.assertCreationBefore("tag"); + } + + private static final class RecordingListener implements AuditListener { + private final List events = new ArrayList<>(); + + @Override + public void onContextCreatedMeta(String contextId, String algorithmId, String provider, KeyUsage role, + String keyFingerprint, Map specMeta) { + events.add("meta"); + } + + @Override + public void onProgress(String contextId, long bodyBytes, long trailerBytes) { + events.add("progress"); + } + + @Override + public void onTagProduced(String contextId, int tagLength, String policy) { + events.add("tag"); + } + + @Override + public void onAgreementPeerSet(String contextId, String peerFingerprint) { + events.add("peer"); + } + + @Override + public void onAgreementDerived(String contextId, int secretLength) { + events.add("derived"); + } + + @Override + public void onAgreementPeerMessageSet(String contextId, int messageLength) { + events.add("message-set"); + } + + @Override + public void onAgreementPeerMessageGet(String contextId, int messageLength) { + events.add("message-get"); + } + + @Override + public void onKemEncapsulated(int ciphertextLength, int sharedSecretLength) { + events.add("encapsulated"); + } + + @Override + public void onKemDecapsulated(int sharedSecretLength) { + events.add("decapsulated"); + } + + @Override + public void onFailure(String contextId, String stage, String operation, Throwable cause) { + events.add("failure"); + } + + private int count(String event) { + return (int) events.stream().filter(event::equals).count(); + } + + private void assertCreationBefore(String event) { + assertEquals("meta", events.get(0)); + assertEquals(1, count("meta")); + assertTrue(events.indexOf(event) > 0); + } + + private void assertBefore(String first, String second) { + assertTrue(events.indexOf(first) < events.indexOf(second)); + } + } +} diff --git a/lib/src/test/java/zeroecho/core/CryptoArchitectureTest.java b/lib/src/test/java/zeroecho/core/CryptoArchitectureTest.java new file mode 100644 index 0000000..bf11418 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/CryptoArchitectureTest.java @@ -0,0 +1,205 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Proxy; +import java.security.Key; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.audit.AuditMode; +import zeroecho.core.audit.AuditListener; +import zeroecho.core.context.DigestContext; +import zeroecho.core.policy.CryptoPolicy; +import zeroecho.core.spec.ContextSpec; +import zeroecho.sdk.ZeroEchoSession; + +/** + * Verifies authoritative registry ownership and explicitly scoped runtime state. + */ +class CryptoArchitectureTest { + + @Test + void authoritativeRegistry() { + System.out.println("authoritativeRegistry"); + CryptoCatalog catalog = CryptoCatalog.load(); + + assertSame(CryptoAlgorithms.registry(), catalog.algorithms()); + assertEquals(CryptoAlgorithms.available(), catalog.algorithms().keySet()); + for (String id : CryptoAlgorithms.available()) { + assertSame(CryptoAlgorithms.require(id), catalog.algorithms().get(id)); + } + + System.out.println("...providerCount=" + CryptoAlgorithms.available().size()); + System.out.println("authoritativeRegistry...ok"); + } + + @Test + void explicitPolicyOrder() throws Exception { + System.out.println("explicitPolicyOrder"); + List events = new ArrayList<>(); + AuditListener listener = policyOrderListener(events); + ZeroEchoSession allowed = new ZeroEchoSession().withAuditListener(listener).withPolicy( + (id, role, key, spec) -> events.add("policy")); + try (DigestContext context = allowed.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE)) { + assertSame(CryptoAlgorithms.require("DIGEST"), context.algorithm()); + } + assertEquals(List.of("policy", "audit"), events); + + events.clear(); + IllegalArgumentException denial = new IllegalArgumentException("controlled denial"); + ZeroEchoSession denied = new ZeroEchoSession().withAuditListener(listener).withPolicy( + (id, role, key, spec) -> { + events.add("policy"); + throw denial; + }); + assertSame(denial, assertThrows(IllegalArgumentException.class, + () -> denied.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE))); + assertEquals(List.of("policy"), events); + + events.clear(); + IllegalStateException failure = new IllegalStateException("controlled policy failure"); + ZeroEchoSession failing = new ZeroEchoSession().withAuditListener(listener).withPolicy( + (id, role, key, spec) -> { + events.add("policy"); + throw failure; + }); + assertSame(failure, assertThrows(IllegalStateException.class, + () -> failing.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE))); + assertEquals(List.of("policy"), events); + + System.out.println("...allow=1...deny=1...failure=1"); + System.out.println("explicitPolicyOrder...ok"); + } + + @Test + void isolatedAuditConfiguration() throws Exception { + System.out.println("isolatedAuditConfiguration"); + AtomicInteger firstEvents = new AtomicInteger(); + AtomicInteger secondEvents = new AtomicInteger(); + AuditListener firstListener = contextListener(firstEvents); + AuditListener secondListener = contextListener(secondEvents); + ZeroEchoSession wrapped = new ZeroEchoSession().withAuditListener(firstListener) + .withAuditMode(AuditMode.WRAP); + ZeroEchoSession direct = new ZeroEchoSession().withAuditListener(secondListener); + + try (DigestContext wrappedContext = wrapped.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE)) { + assertTrue(Proxy.isProxyClass(wrappedContext.getClass())); + } + int wrappedEvents = firstEvents.get(); + assertTrue(wrappedEvents > 0); + assertEquals(0, secondEvents.get()); + try (DigestContext directContext = direct.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE)) { + assertTrue(!Proxy.isProxyClass(directContext.getClass())); + } + assertEquals(wrappedEvents, firstEvents.get()); + assertEquals(1, secondEvents.get()); + assertEquals(AuditMode.WRAP, wrapped.auditMode()); + assertEquals(AuditMode.OFF, direct.auditMode()); + + System.out.println("...firstEvents=" + firstEvents.get()); + System.out.println("...secondEvents=" + secondEvents.get()); + System.out.println("isolatedAuditConfiguration...ok"); + } + + @Test + void immutableSessionChanges() { + System.out.println("immutableSessionChanges"); + ZeroEchoSession base = new ZeroEchoSession(); + ZeroEchoSession changed = base.withAuditMode(AuditMode.WRAP) + .withAuditListener(contextListener(new AtomicInteger())); + + assertNotSame(base, changed); + assertEquals(AuditMode.OFF, base.auditMode()); + assertEquals(AuditMode.WRAP, changed.auditMode()); + assertNotSame(base.auditListener(), changed.auditListener()); + + System.out.println("...baseMode=" + base.auditMode()); + System.out.println("...changedMode=" + changed.auditMode()); + System.out.println("immutableSessionChanges...ok"); + } + + @Test + void concurrentSessionReads() throws Exception { + System.out.println("concurrentSessionReads"); + ZeroEchoSession session = new ZeroEchoSession().withAuditMode(AuditMode.MANUAL); + int taskCount = 16; + CountDownLatch start = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(8); + try { + List> futures = new ArrayList<>(); + for (int index = 0; index < taskCount; index++) { + futures.add(executor.submit(() -> { + start.await(); + for (int iteration = 0; iteration < 500; iteration++) { + if (session.auditMode() != AuditMode.MANUAL || session.available().isEmpty() + || session.require("DIGEST") != CryptoAlgorithms.require("DIGEST")) { + return Boolean.FALSE; + } + } + return Boolean.TRUE; + })); + } + start.countDown(); + for (Future future : futures) { + assertTrue(future.get().booleanValue()); + } + } finally { + executor.shutdownNow(); + } + + System.out.println("...completedTasks=" + taskCount); + System.out.println("concurrentSessionReads...ok"); + } + + @Test + void manualModeSilent() throws Exception { + System.out.println("manualModeSilent"); + AtomicInteger events = new AtomicInteger(); + ZeroEchoSession session = new ZeroEchoSession().withAuditListener(contextListener(events)) + .withAuditMode(AuditMode.MANUAL); + + try (DigestContext context = session.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE)) { + assertTrue(!Proxy.isProxyClass(context.getClass())); + } + + assertEquals(0, events.get()); + System.out.println("...events=" + events.get()); + System.out.println("manualModeSilent...ok"); + } + + private static AuditListener contextListener(AtomicInteger count) { + return new AuditListener() { + @Override + public void onContextCreatedMeta(String contextId, String algorithmId, String provider, KeyUsage role, + String keyFingerprint, java.util.Map specMeta) { + count.incrementAndGet(); + } + }; + } + + private static AuditListener policyOrderListener(List events) { + return new AuditListener() { + @Override + public void onContextCreatedMeta(String contextId, String algorithmId, String provider, KeyUsage role, + String keyFingerprint, java.util.Map specMeta) { + events.add("audit"); + } + }; + } +} diff --git a/lib/src/test/java/zeroecho/core/SecretSpecLifecycleTest.java b/lib/src/test/java/zeroecho/core/SecretSpecLifecycleTest.java new file mode 100644 index 0000000..86b464d --- /dev/null +++ b/lib/src/test/java/zeroecho/core/SecretSpecLifecycleTest.java @@ -0,0 +1,244 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import javax.security.auth.Destroyable; +import javax.crypto.SecretKey; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.alg.aes.AesKeyImportSpec; +import zeroecho.core.alg.aes.AesAlgorithm; +import zeroecho.core.alg.chacha.ChaChaKeyImportSpec; +import zeroecho.core.alg.hmac.HmacKeyImportSpec; +import zeroecho.core.alg.mldsa.MldsaPrivateKeySpec; +import zeroecho.core.alg.rsa.RsaPrivateKeySpec; +import zeroecho.core.alg.slhdsa.SlhDsaPrivateKeySpec; +import zeroecho.core.alg.sphincsplus.SphincsPlusPrivateKeySpec; +import zeroecho.core.marshal.PairSeq; + +class SecretSpecLifecycleTest { + private static final List SPECS = List.of( + new SpecCase("zeroecho.core.alg.aes.AesKeyImportSpec", "key", 16, Factory.STATIC_RAW), + new SpecCase("zeroecho.core.alg.bike.BikePrivateKeySpec", "pkcs8", 8, Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.chacha.ChaChaKeyImportSpec", "key", 32, Factory.STATIC_RAW), + new SpecCase("zeroecho.core.alg.cmce.CmcePrivateKeySpec", "pkcs8", 8, Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.dh.DhPrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.ecdsa.EcdsaPrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.ed25519.Ed25519PrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.ed448.Ed448PrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.elgamal.ElgamalPrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.frodo.FrodoPrivateKeySpec", "pkcs8", 8, Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.hmac.HmacKeyImportSpec", "key", 8, Factory.HMAC), + new SpecCase("zeroecho.core.alg.hqc.HqcPrivateKeySpec", "pkcs8", 8, Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.kyber.KyberPrivateKeySpec", "pkcs8", 8, Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.mldsa.MldsaPrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.ntru.NtruPrivateKeySpec", "pkcs8", 8, Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.ntruprime.NtrulPrimePrivateKeySpec", "pkcs8", 8, + Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.ntruprime.SntruPrimePrivateKeySpec", "pkcs8", 8, + Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.rsa.RsaPrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.saber.SaberPrivateKeySpec", "pkcs8", 8, Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.slhdsa.SlhDsaPrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.sphincsplus.SphincsPlusPrivateKeySpec", "encoded", 8, + Factory.CONSTRUCTOR), + new SpecCase("zeroecho.core.alg.xdh.XdhPrivateKeySpec", "encoded", 8, Factory.CONSTRUCTOR)); + + @Test + void allSecretSpecificationsOwnAndDestroyTheirByteArrays() throws Exception { + System.out.print("SecretSpec/lifecycle..."); + for (SpecCase specCase : SPECS) { + byte[] input = sequence(specCase.length()); + byte[] expected = input.clone(); + Object spec = specCase.create(input); + Arrays.fill(input, (byte) 0); + + Method accessor = spec.getClass().getMethod(specCase.accessor()); + byte[] first = (byte[]) accessor.invoke(spec); + assertArrayEquals(expected, first, specCase.className()); + first[0] ^= 0x7f; + assertArrayEquals(expected, (byte[]) accessor.invoke(spec), specCase.className()); + + Method marshal = spec.getClass().getMethod("marshal", spec.getClass()); + marshal.invoke(null, spec); + Destroyable destroyable = assertInstanceOf(Destroyable.class, spec); + assertFalse(destroyable.isDestroyed()); + destroyable.destroy(); + destroyable.destroy(); + assertTrue(destroyable.isDestroyed()); + assertAllSecretFieldsZero(spec); + + InvocationTargetException accessFailure = assertThrows(InvocationTargetException.class, + () -> accessor.invoke(spec), specCase.className()); + assertInstanceOf(IllegalStateException.class, accessFailure.getCause()); + InvocationTargetException marshalFailure = assertThrows(InvocationTargetException.class, + () -> marshal.invoke(null, spec), specCase.className()); + assertInstanceOf(IllegalStateException.class, marshalFailure.getCause()); + } + System.out.println("ok"); + } + + @Test + void validatesNullAndBoundaryKeyMaterial() { + System.out.print("SecretSpec/boundaries..."); + assertThrows(NullPointerException.class, () -> AesKeyImportSpec.fromRaw(null)); + assertThrows(IllegalArgumentException.class, () -> AesKeyImportSpec.fromRaw(new byte[0])); + assertThrows(NullPointerException.class, () -> new HmacKeyImportSpec("HmacSHA256", null)); + assertThrows(NullPointerException.class, () -> new RsaPrivateKeySpec(null)); + + HmacKeyImportSpec emptyHmac = new HmacKeyImportSpec("HmacSHA256", new byte[0]); + RsaPrivateKeySpec emptyRsa = new RsaPrivateKeySpec(new byte[0]); + assertArrayEquals(new byte[0], emptyHmac.key()); + assertArrayEquals(new byte[0], emptyRsa.encoded()); + emptyHmac.destroy(); + emptyRsa.destroy(); + System.out.println("ok"); + } + + @Test + void unmarshalCleansUpWhenMalformedDataFollowsValidSecretMaterial() { + System.out.println("unmarshalCleansUpWhenMalformedDataFollowsValidSecretMaterial"); + String aesKey = Base64.getEncoder().encodeToString(sequence(16)); + String chachaKey = Base64.getEncoder().encodeToString(sequence(32)); + String encodedPrivateKey = Base64.getEncoder().encodeToString(sequence(8)); + + assertThrows(IllegalArgumentException.class, + () -> AesKeyImportSpec.unmarshal(PairSeq.of("k.b64", aesKey, "k.b64", "%"))); + assertThrows(IllegalArgumentException.class, + () -> ChaChaKeyImportSpec.unmarshal(PairSeq.of("k.b64", chachaKey, "k.b64", "%"))); + assertThrows(IllegalArgumentException.class, () -> HmacKeyImportSpec.unmarshal( + PairSeq.of("mac", "HmacSHA256", "k.b64", encodedPrivateKey, "k.b64", "%"))); + assertThrows(IllegalArgumentException.class, () -> MldsaPrivateKeySpec.unmarshal( + PairSeq.of("pkcs8.b64", encodedPrivateKey, "pkcs8.b64", "%"))); + assertThrows(IllegalArgumentException.class, () -> SlhDsaPrivateKeySpec.unmarshal( + PairSeq.of("pkcs8.b64", encodedPrivateKey, "pkcs8.b64", "%"))); + assertThrows(IllegalArgumentException.class, () -> SphincsPlusPrivateKeySpec.unmarshal( + PairSeq.of("pkcs8.b64", encodedPrivateKey, "pkcs8.b64", "%"))); + System.out.println("...cases=6"); + System.out.println("unmarshalCleansUpWhenMalformedDataFollowsValidSecretMaterial...ok"); + } + + @Test + void accessAndDestroyAreLinearizable() throws Exception { + System.out.print("SecretSpec/concurrent..."); + byte[] expected = sequence(16); + AesKeyImportSpec spec = AesKeyImportSpec.fromRaw(expected); + CountDownLatch start = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(8); + try { + List> futures = new ArrayList<>(); + for (int i = 0; i < 32; i++) { + futures.add(executor.submit(() -> { + start.await(); + try { + assertArrayEquals(expected, spec.key()); + } catch (IllegalStateException destroyed) { + assertTrue(spec.isDestroyed()); + } + return null; + })); + } + futures.add(executor.submit(() -> { + start.await(); + spec.destroy(); + return null; + })); + start.countDown(); + for (Future future : futures) { + future.get(); + } + } finally { + executor.shutdownNow(); + } + assertTrue(spec.isDestroyed()); + assertThrows(IllegalStateException.class, spec::key); + System.out.println("ok"); + } + + @Test + void importersDoNotDestroyCallerOwnedSpecifications() throws Exception { + System.out.print("SecretSpec/import..."); + byte[] aesBytes = sequence(16); + AesKeyImportSpec aesSpec = AesKeyImportSpec.fromRaw(aesBytes); + AesAlgorithm aes = new AesAlgorithm(); + SecretKey secretKey = aes.symmetricKeyImporter(AesKeyImportSpec.class).importSecret(aesSpec); + assertArrayEquals(aesBytes, secretKey.getEncoded()); + assertFalse(aesSpec.isDestroyed()); + assertArrayEquals(aesBytes, aesSpec.key()); + + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + KeyPair pair = generator.generateKeyPair(); + RsaPrivateKeySpec rsaSpec = new RsaPrivateKeySpec(pair.getPrivate().getEncoded()); + PrivateKey imported = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().importPrivate("RSA", rsaSpec); + assertArrayEquals(pair.getPrivate().getEncoded(), imported.getEncoded()); + assertFalse(rsaSpec.isDestroyed()); + assertArrayEquals(pair.getPrivate().getEncoded(), rsaSpec.encoded()); + System.out.println("ok"); + } + + private static byte[] sequence(int length) { + byte[] bytes = new byte[length]; + for (int i = 0; i < length; i++) { + bytes[i] = (byte) (i + 1); + } + return bytes; + } + + private static void assertAllSecretFieldsZero(Object spec) throws IllegalAccessException { + for (Field field : spec.getClass().getDeclaredFields()) { + if (field.getType() == byte[].class) { + field.setAccessible(true); + byte[] bytes = (byte[]) field.get(spec); + assertTrue(Arrays.equals(new byte[bytes.length], bytes), spec.getClass().getName()); + } + } + } + + private enum Factory { + CONSTRUCTOR, + STATIC_RAW, + HMAC + } + + private record SpecCase(String className, String accessor, int length, Factory factory) { + private Object create(byte[] input) throws ReflectiveOperationException { + Class type = Class.forName(className); + if (factory == Factory.STATIC_RAW) { + Method method = type.getMethod("fromRaw", byte[].class); + return method.invoke(null, (Object) input); + } + if (factory == Factory.HMAC) { + Constructor constructor = type.getConstructor(String.class, byte[].class); + return constructor.newInstance("HmacSHA256", input); + } + Constructor constructor = type.getConstructor(byte[].class); + return constructor.newInstance((Object) input); + } + } +} diff --git a/lib/src/test/java/zeroecho/core/TargetArchitectureTest.java b/lib/src/test/java/zeroecho/core/TargetArchitectureTest.java new file mode 100644 index 0000000..afd61d5 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/TargetArchitectureTest.java @@ -0,0 +1,154 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +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.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext; +import zeroecho.core.audit.AuditListener; +import zeroecho.core.audit.AuditMode; +import zeroecho.core.context.DigestContext; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.sdk.ZeroEchoSession; + +/** + * Guards the consolidated pre-release architecture against legacy API drift. + */ +class TargetArchitectureTest { + + @Test + void exactRegisteredKeyOperationMatrix() { + String name = start("exactRegisteredKeyOperationMatrix"); + Map counts = new EnumMap<>(KeyOperation.class); + Set registrations = new HashSet<>(); + + for (String algorithmId : CryptoAlgorithms.available()) { + CryptoAlgorithm algorithm = CryptoAlgorithms.require(algorithmId); + for (KeyOperationInfo info : algorithm.keyOperations()) { + String registration = algorithmId + "|" + info.operation() + "|" + info.specType().getName(); + assertTrue(registrations.add(registration), registration); + assertLookupSucceeds(algorithm, info); + counts.merge(info.operation(), 1, Integer::sum); + } + } + + assertEquals(20, counts.getOrDefault(KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE, 0)); + assertEquals(20, counts.getOrDefault(KeyOperation.ASYMMETRIC_PUBLIC_IMPORT, 0)); + assertEquals(20, counts.getOrDefault(KeyOperation.ASYMMETRIC_PRIVATE_IMPORT, 0)); + assertEquals(4, counts.getOrDefault(KeyOperation.SYMMETRIC_GENERATE, 0)); + assertEquals(4, counts.getOrDefault(KeyOperation.SYMMETRIC_IMPORT, 0)); + progress("registrations=" + registrations.size()); + ok(name); + } + + @Test + void obsoleteArchitectureTypesAreAbsent() { + String name = start("obsoleteArchitectureTypesAreAbsent"); + assertThrows(ClassNotFoundException.class, + () -> Class.forName("zeroecho.core.spi.ContextConstructorKS")); + assertThrows(ClassNotFoundException.class, + () -> Class.forName("zeroecho.core.spi.SymmetricKeyBuilder")); + assertThrows(ClassNotFoundException.class, + () -> Class.forName("zeroecho.core.spi.AsymmetricKeyBuilder")); + + for (Method method : CryptoAlgorithms.class.getDeclaredMethods()) { + if (Modifier.isPublic(method.getModifiers()) && Modifier.isStatic(method.getModifiers())) { + assertTrue(method.getName().equals("available") || method.getName().equals("require"), + method.toString()); + } + } + for (Field field : CryptoAlgorithms.class.getDeclaredFields()) { + assertTrue(Modifier.isFinal(field.getModifiers()), field.toString()); + } + Set removedConveniences = Set.of("generateSecret", "importSecret", "generateKeyPair", + "importPublic", "importPrivate"); + for (Method method : CryptoAlgorithm.class.getDeclaredMethods()) { + assertFalse(removedConveniences.contains(method.getName()), method.toString()); + } + progress("registryStatics=readOnly"); + ok(name); + } + + @Test + void agreementImplementationIsFinal() { + String name = start("agreementImplementationIsFinal"); + assertTrue(Modifier.isFinal(GenericJcaAgreementContext.class.getModifiers())); + progress("final=true"); + ok(name); + } + + @Test + void auditListenerFailureCannotChangeOperationOutcome() throws Exception { + String name = start("auditListenerFailureCannotChangeOperationOutcome"); + AuditListener failing = new AuditListener() { + @Override + public void onContextCreatedMeta(String contextId, String algorithmId, String provider, + KeyUsage role, String keyFingerprint, Map metadata) { + throw new AssertionError("controlled listener failure"); + } + + @Override + public void onContextClosed(String contextId, long bodyBytes, long trailerBytes, + long durationMillis) { + throw new AssertionError("controlled listener failure"); + } + }; + + for (AuditMode mode : new AuditMode[] { AuditMode.OFF, AuditMode.WRAP }) { + ZeroEchoSession session = new ZeroEchoSession().withAuditListener(failing).withAuditMode(mode); + try (DigestContext context = assertDoesNotThrow( + () -> session.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE))) { + assertFalse(context.algorithm().id().isBlank()); + } + } + progress("modes=OFF,WRAP"); + ok(name); + } + + @SuppressWarnings("unchecked") + private static void assertLookupSucceeds(CryptoAlgorithm algorithm, KeyOperationInfo info) { + Class specType = (Class) info.specType(); + switch (info.operation()) { + case ASYMMETRIC_KEY_PAIR_GENERATE -> + assertDoesNotThrow(() -> algorithm.asymmetricKeyPairGenerator(specType)); + case ASYMMETRIC_PUBLIC_IMPORT -> + assertDoesNotThrow(() -> algorithm.publicKeyImporter(specType)); + case ASYMMETRIC_PRIVATE_IMPORT -> + assertDoesNotThrow(() -> algorithm.privateKeyImporter(specType)); + case SYMMETRIC_GENERATE -> + assertDoesNotThrow(() -> algorithm.symmetricKeyGenerator(specType)); + case SYMMETRIC_IMPORT -> + assertDoesNotThrow(() -> algorithm.symmetricKeyImporter(specType)); + } + } + + private static String start(String routine) { + String label = routine.length() <= 30 ? routine : routine.substring(0, 27) + "..."; + System.out.println(label); + return label; + } + + private static void progress(String detail) { + System.out.println("..." + detail); + } + + private static void ok(String name) { + System.out.println(name + "...ok"); + } +} diff --git a/lib/src/test/java/zeroecho/core/WrongKeySecurityTest.java b/lib/src/test/java/zeroecho/core/WrongKeySecurityTest.java new file mode 100644 index 0000000..70968f0 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/WrongKeySecurityTest.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.security.Key; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.audit.AuditMode; +import zeroecho.core.audit.AuditListener; +import zeroecho.core.err.UnsupportedSpecException; +import zeroecho.core.spec.ContextSpec; +import zeroecho.sdk.ZeroEchoSession; + +class WrongKeySecurityTest { + + @Test + void wrongKeyMatrix() { + System.out.println("wrongKeyMatrix"); + AtomicInteger auditEvents = new AtomicInteger(); + ZeroEchoSession session = new ZeroEchoSession().withAuditMode(AuditMode.WRAP) + .withAuditListener(new AuditListener() { + @Override + public void onContextCreatedMeta(String contextId, String algorithmId, String provider, + KeyUsage role, String keyFingerprint, java.util.Map specMeta) { + auditEvents.incrementAndGet(); + } + }); + + assertWrongKey(session, "AES", KeyUsage.ENCRYPT); + assertWrongKey(session, "ECDSA", KeyUsage.SIGN); + assertWrongKey(session, "Xdh", KeyUsage.AGREEMENT); + assertWrongKey(session, "ML-KEM", KeyUsage.ENCAPSULATE); + assertEquals(0, auditEvents.get(), "rejected keys must not create or process contexts"); + + System.out.println("...families=4"); + System.out.println("wrongKeyMatrix...ok"); + } + + private static void assertWrongKey(ZeroEchoSession session, String algorithm, KeyUsage role) { + UnsupportedSpecException failure = assertThrows(UnsupportedSpecException.class, + () -> session.createContext(algorithm, role, NullKey.INSTANCE)); + assertTrue(failure.getMessage().contains(algorithm)); + assertTrue(failure.getMessage().contains(role.name())); + assertNull(failure.getCause()); + } +} diff --git a/lib/src/test/java/zeroecho/core/ZeroEchoSessionWrapIntegrationTest.java b/lib/src/test/java/zeroecho/core/ZeroEchoSessionWrapIntegrationTest.java new file mode 100644 index 0000000..070f8dc --- /dev/null +++ b/lib/src/test/java/zeroecho/core/ZeroEchoSessionWrapIntegrationTest.java @@ -0,0 +1,275 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Proxy; +import java.security.KeyPair; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import conflux.Ctx; +import conflux.CtxInterface; +import zeroecho.core.audit.AuditMode; +import zeroecho.core.alg.aes.AesSpec; +import zeroecho.core.alg.common.agreement.KeyPairKey; +import zeroecho.core.alg.ed25519.Ed25519KeyGenSpec; +import zeroecho.core.alg.hmac.HmacSpec; +import zeroecho.core.alg.kyber.KyberKeyGenSpec; +import zeroecho.core.alg.xdh.XdhSpec; +import zeroecho.core.audit.AuditListener; +import zeroecho.core.context.AgreementContext; +import zeroecho.core.context.CryptoContext; +import zeroecho.core.context.DigestContext; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.MacContext; +import zeroecho.core.context.MessageAgreementContext; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.io.TailStrippingInputStream; +import zeroecho.core.spec.VoidSpec; +import zeroecho.core.spi.ContextAware; +import zeroecho.core.tag.TagEngine; +import zeroecho.sdk.ZeroEchoSession; +import zeroecho.sdk.util.BouncyCastleActivator; + +class ZeroEchoSessionWrapIntegrationTest { + private static final AtomicInteger CONTEXT_IDS = new AtomicInteger(); + + @BeforeAll + static void initializeProvider() { + BouncyCastleActivator.init(); + } + + @Test + void registeredStreamContexts() throws Exception { + System.out.println("registeredStreamContexts"); + RecordingListener listener = new RecordingListener(); + ZeroEchoSession session = wrappedSession(listener); + byte[] message = { 1, 2, 3, 4, 5 }; + + SecretKey aesKey = new SecretKeySpec(new byte[16], "AES"); + CtxInterface aesContext = Ctx.INSTANCE.getContext("wrap-integration-" + CONTEXT_IDS.incrementAndGet()); + byte[] ciphertext; + try (EncryptionContext encryption = session.createContext( + "AES", KeyUsage.ENCRYPT, aesKey, AesSpec.gcm128(null))) { + assertProxy(encryption); + ((ContextAware) encryption).setContext(aesContext); + try (InputStream input = encryption.attach(new ByteArrayInputStream(message))) { + ciphertext = input.readAllBytes(); + } + } + try (EncryptionContext decryption = session.createContext( + "AES", KeyUsage.DECRYPT, aesKey, AesSpec.gcm128(null))) { + assertProxy(decryption); + ((ContextAware) decryption).setContext(aesContext); + try (InputStream input = decryption.attach(new ByteArrayInputStream(ciphertext))) { + assertArrayEquals(message, input.readAllBytes()); + } + } + + KeyPair signingKeys = session.keyBuilders().asymmetric() + .generateKeyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec()); + TaggedBody signature; + try (SignatureContext signer = session.createContext( + "Ed25519", KeyUsage.SIGN, signingKeys.getPrivate())) { + assertProxy(signer); + signature = produceTag(signer, message); + } + try (SignatureContext verifier = session.createContext( + "Ed25519", KeyUsage.VERIFY, signingKeys.getPublic())) { + assertProxy(verifier); + verifier.setExpectedTag(signature.tag()); + try (InputStream input = verifier.wrap(new ByteArrayInputStream(message))) { + assertArrayEquals(message, input.readAllBytes()); + } + } + + SecretKey hmacKey = new SecretKeySpec(new byte[32], "HmacSHA256"); + try (MacContext mac = session.createContext("HMAC", KeyUsage.MAC, hmacKey, HmacSpec.sha256())) { + assertProxy(mac); + assertArrayEquals(message, produceTag(mac, message).body()); + } + try (DigestContext digest = session.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE)) { + assertProxy(digest); + assertArrayEquals(message, produceTag(digest, message).body()); + } + + assertEquals(List.of( + "create:ENCRYPT", "create:DECRYPT", + "create:SIGN", "tag", "create:VERIFY", "verify:true", + "create:MAC", "tag", "create:DIGEST", "tag"), listener.events); + System.out.println("...contexts=6...events=" + listener.events.size()); + System.out.println("registeredStreamContexts...ok"); + } + + @Test + void registeredAgreementContexts() throws Exception { + System.out.println("registeredAgreementContexts"); + RecordingListener listener = new RecordingListener(); + ZeroEchoSession session = wrappedSession(listener); + KeyPair aliceKeys = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobKeys = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + + try (AgreementContext alice = session.createContext( + "Xdh", KeyUsage.AGREEMENT, aliceKeys.getPrivate(), XdhSpec.X25519); + AgreementContext bob = session.createContext( + "Xdh", KeyUsage.AGREEMENT, bobKeys.getPrivate(), XdhSpec.X25519)) { + assertProxy(alice); + assertProxy(bob); + alice.setPeerPublic(bobKeys.getPublic()); + bob.setPeerPublic(aliceKeys.getPublic()); + assertArrayEquals(alice.deriveSecret(), bob.deriveSecret()); + } + + try (MessageAgreementContext alice = session.createContext( + "Xdh", KeyUsage.AGREEMENT, new KeyPairKey(aliceKeys), XdhSpec.X25519); + MessageAgreementContext bob = session.createContext( + "Xdh", KeyUsage.AGREEMENT, new KeyPairKey(bobKeys), XdhSpec.X25519)) { + assertProxy(alice); + assertProxy(bob); + byte[] aliceMessage = alice.getPeerMessage(); + byte[] bobMessage = bob.getPeerMessage(); + alice.setPeerMessage(bobMessage); + bob.setPeerMessage(aliceMessage); + assertArrayEquals(alice.deriveSecret(), bob.deriveSecret()); + } + + assertEquals(List.of( + "create:AGREEMENT", "create:AGREEMENT", "peer", "peer", "derived", "derived", + "create:AGREEMENT", "create:AGREEMENT", "message-get", "message-get", + "message-set", "message-set", "derived", "derived"), listener.events); + System.out.println("...contexts=4...events=" + listener.events.size()); + System.out.println("registeredAgreementContexts...ok"); + } + + @Test + void registeredKemContexts() throws Exception { + System.out.println("registeredKemContexts"); + RecordingListener listener = new RecordingListener(); + ZeroEchoSession session = wrappedSession(listener); + KeyPair recipient = session.keyBuilders().asymmetric() + .generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512()); + + KemContext.KemResult encapsulated; + try (KemContext encapsulator = session.createContext( + "ML-KEM", KeyUsage.ENCAPSULATE, recipient.getPublic(), VoidSpec.INSTANCE); + KemContext decapsulator = session.createContext( + "ML-KEM", KeyUsage.DECAPSULATE, recipient.getPrivate(), VoidSpec.INSTANCE)) { + assertProxy(encapsulator); + assertProxy(decapsulator); + encapsulated = encapsulator.encapsulate(); + assertArrayEquals(encapsulated.sharedSecret(), decapsulator.decapsulate(encapsulated.ciphertext())); + } + + try (MessageAgreementContext initiator = session.createContext( + "ML-KEM", KeyUsage.AGREEMENT, recipient.getPublic(), VoidSpec.INSTANCE); + MessageAgreementContext responder = session.createContext( + "ML-KEM", KeyUsage.AGREEMENT, recipient.getPrivate(), VoidSpec.INSTANCE)) { + assertProxy(initiator); + assertProxy(responder); + responder.setPeerMessage(initiator.getPeerMessage()); + assertArrayEquals(initiator.deriveSecret(), responder.deriveSecret()); + } + + assertEquals(List.of( + "create:ENCAPSULATE", "create:DECAPSULATE", "encapsulated", "decapsulated", + "create:AGREEMENT", "create:AGREEMENT", "message-get", "message-set", "derived", "derived"), + listener.events); + System.out.println("...contexts=4...events=" + listener.events.size()); + System.out.println("registeredKemContexts...ok"); + } + + private static ZeroEchoSession wrappedSession(RecordingListener listener) { + return new ZeroEchoSession().withAuditListener(listener).withAuditMode(AuditMode.WRAP); + } + + private static TaggedBody produceTag(TagEngine engine, byte[] body) throws IOException { + byte[][] tag = new byte[1][]; + int tagLength = engine.tagLength(); + byte[] emittedBody; + try (InputStream input = new TailStrippingInputStream( + engine.wrap(new ByteArrayInputStream(body)), tagLength, 128) { + @Override + protected void processTail(byte[] tail) { + tag[0] = tail.clone(); + } + }) { + emittedBody = input.readAllBytes(); + } + return new TaggedBody(emittedBody, tag[0]); + } + + private static void assertProxy(CryptoContext context) { + assertTrue(Proxy.isProxyClass(context.getClass())); + } + + private record TaggedBody(byte[] body, byte[] tag) {} + + private static final class RecordingListener implements AuditListener { + private final List events = new ArrayList<>(); + + @Override + public void onContextCreatedMeta(String contextId, String algorithmId, String provider, KeyUsage role, + String keyFingerprint, Map specMeta) { + events.add("create:" + role); + } + + @Override + public void onTagProduced(String contextId, int tagLength, String policy) { + events.add("tag"); + } + + @Override + public void onVerifyResult(String contextId, boolean success, String policy, String expectedSource, + int tagLength) { + events.add("verify:" + success); + } + + @Override + public void onAgreementPeerSet(String contextId, String peerFingerprint) { + events.add("peer"); + } + + @Override + public void onAgreementDerived(String contextId, int secretLength) { + events.add("derived"); + } + + @Override + public void onAgreementPeerMessageSet(String contextId, int messageLength) { + events.add("message-set"); + } + + @Override + public void onAgreementPeerMessageGet(String contextId, int messageLength) { + events.add("message-get"); + } + + @Override + public void onKemEncapsulated(int ciphertextLength, int sharedSecretLength) { + events.add("encapsulated"); + } + + @Override + public void onKemDecapsulated(int sharedSecretLength) { + events.add("decapsulated"); + } + } +} diff --git a/lib/src/test/java/zeroecho/core/alg/aes/AesDecryptionSecurityTest.java b/lib/src/test/java/zeroecho/core/alg/aes/AesDecryptionSecurityTest.java new file mode 100644 index 0000000..bb6d029 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/alg/aes/AesDecryptionSecurityTest.java @@ -0,0 +1,233 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core.alg.aes; + +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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.crypto.BadPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.junit.jupiter.api.Test; + +import conflux.Ctx; +import conflux.CtxInterface; +import zeroecho.core.ConfluxKeys; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.spi.ContextAware; + +class AesDecryptionSecurityTest { + private static final SecretKey KEY = new SecretKeySpec(repeated((byte) 0x11, 16), "AES"); + private static final SecretKey WRONG_KEY = new SecretKeySpec(repeated((byte) 0x22, 16), "AES"); + private static final AtomicInteger CONTEXT_IDS = new AtomicInteger(); + + @Test + void gcmTamperMatrix() throws Exception { + System.out.println("gcmTamperMatrix"); + byte[] plaintext = repeated((byte) 0x5a, 96); + Encrypted encrypted = encrypt(AesSpec.gcm128(null), plaintext); + + FailureResult modifiedBody = failedDecryption( + encrypted.spec, KEY, encrypted.iv, changed(encrypted.ciphertext, 0)); + FailureResult modifiedTag = failedDecryption(encrypted.spec, KEY, encrypted.iv, + changed(encrypted.ciphertext, encrypted.ciphertext.length - 1)); + FailureResult truncatedBody = failedDecryption(encrypted.spec, KEY, encrypted.iv, + removed(encrypted.ciphertext, encrypted.ciphertext.length - 17)); + FailureResult truncatedTag = failedDecryption(encrypted.spec, KEY, encrypted.iv, + Arrays.copyOf(encrypted.ciphertext, encrypted.ciphertext.length - 1)); + FailureResult trailing = failedDecryption(encrypted.spec, KEY, encrypted.iv, + Arrays.copyOf(encrypted.ciphertext, encrypted.ciphertext.length + 1)); + FailureResult wrongKey = failedDecryption(encrypted.spec, WRONG_KEY, encrypted.iv, encrypted.ciphertext); + FailureResult wrongIv = failedDecryption( + encrypted.spec, KEY, changed(encrypted.iv, 0), encrypted.ciphertext); + FailureResult empty = failedDecryption(encrypted.spec, KEY, encrypted.iv, new byte[0]); + + for (FailureResult result : List.of( + modifiedBody, modifiedTag, truncatedBody, truncatedTag, trailing, wrongKey, wrongIv, empty)) { + assertEquals(0, result.outputBytes()); + assertAuthenticationFailure(result.failure()); + } + assertThrows(IOException.class, () -> decrypt(encrypted.spec, KEY, + Arrays.copyOf(encrypted.iv, encrypted.iv.length - 1), encrypted.ciphertext)); + assertThrows(IOException.class, + () -> decrypt(encrypted.spec, KEY, null, encrypted.ciphertext)); + System.out.println("...cases=10"); + System.out.println("gcmTamperMatrix...ok"); + } + + @Test + void ctrTamperMatrix() throws Exception { + System.out.println("ctrTamperMatrix"); + byte[] plaintext = repeated((byte) 0x33, 64); + Encrypted encrypted = encrypt(AesSpec.ctr(null), plaintext); + + byte[] wrongIv = decrypt(encrypted.spec, KEY, changed(encrypted.iv, 0), encrypted.ciphertext); + byte[] wrongKey = decrypt(encrypted.spec, WRONG_KEY, encrypted.iv, encrypted.ciphertext); + byte[] modified = decrypt(encrypted.spec, KEY, encrypted.iv, changed(encrypted.ciphertext, 0)); + byte[] truncated = decrypt(encrypted.spec, KEY, encrypted.iv, + Arrays.copyOf(encrypted.ciphertext, encrypted.ciphertext.length - 1)); + byte[] trailing = decrypt(encrypted.spec, KEY, encrypted.iv, + Arrays.copyOf(encrypted.ciphertext, encrypted.ciphertext.length + 1)); + + assertFalse(Arrays.equals(plaintext, wrongIv)); + assertFalse(Arrays.equals(plaintext, wrongKey)); + assertFalse(Arrays.equals(plaintext, modified)); + assertEquals(plaintext.length - 1, truncated.length); + assertEquals(plaintext.length + 1, trailing.length); + assertThrows(IOException.class, () -> decrypt(encrypted.spec, KEY, + Arrays.copyOf(encrypted.iv, encrypted.iv.length - 1), encrypted.ciphertext)); + assertThrows(IOException.class, () -> decrypt(encrypted.spec, KEY, null, encrypted.ciphertext)); + System.out.println("...cases=7"); + System.out.println("ctrTamperMatrix...ok"); + } + + @Test + void cbcNoPaddingMatrix() throws Exception { + System.out.println("cbcNoPaddingMatrix"); + AesSpec spec = AesSpec.builder().mode(AesSpec.Mode.CBC).padding(AesSpec.Padding.NOPADDING).build(); + byte[] plaintext = repeated((byte) 0x44, 32); + Encrypted encrypted = encrypt(spec, plaintext); + + assertFalse(Arrays.equals(plaintext, + decrypt(spec, KEY, changed(encrypted.iv, 0), encrypted.ciphertext))); + assertFalse(Arrays.equals(plaintext, + decrypt(spec, WRONG_KEY, encrypted.iv, encrypted.ciphertext))); + assertFalse(Arrays.equals(plaintext, + decrypt(spec, KEY, encrypted.iv, changed(encrypted.ciphertext, 0)))); + assertThrows(IOException.class, () -> decrypt(spec, KEY, encrypted.iv, + Arrays.copyOf(encrypted.ciphertext, encrypted.ciphertext.length - 1))); + assertThrows(IOException.class, () -> decrypt(spec, KEY, encrypted.iv, + Arrays.copyOf(encrypted.ciphertext, encrypted.ciphertext.length + 1))); + assertThrows(IOException.class, () -> decrypt(spec, KEY, + Arrays.copyOf(encrypted.iv, encrypted.iv.length - 1), encrypted.ciphertext)); + assertThrows(IOException.class, () -> decrypt(spec, KEY, null, encrypted.ciphertext)); + System.out.println("...cases=7"); + System.out.println("cbcNoPaddingMatrix...ok"); + } + + @Test + void cbcPkcsPaddingMatrix() throws Exception { + System.out.println("cbcPkcsPaddingMatrix"); + AesSpec spec = AesSpec.cbcPkcs7(null); + Encrypted encrypted = encrypt(spec, repeated((byte) 0x66, 31)); + + assertThrows(IOException.class, () -> decrypt(spec, KEY, encrypted.iv, + Arrays.copyOf(encrypted.ciphertext, encrypted.ciphertext.length - 1))); + // CBC is malleable: flipping this bit changes the final 0x01 padding byte to + // 0x00 deterministically. The assertion covers padding rejection, not + // authentication. + assertThrows(IOException.class, () -> decrypt(spec, KEY, encrypted.iv, + changed(encrypted.ciphertext, encrypted.ciphertext.length - 17))); + assertThrows(IOException.class, () -> decrypt(spec, KEY, + Arrays.copyOf(encrypted.iv, encrypted.iv.length - 1), encrypted.ciphertext)); + assertThrows(IOException.class, () -> decrypt(spec, KEY, null, encrypted.ciphertext)); + System.out.println("...cases=4"); + System.out.println("cbcPkcsPaddingMatrix...ok"); + } + + private static Encrypted encrypt(AesSpec spec, byte[] plaintext) throws Exception { + CtxInterface context = newContext("aes-security-enc-"); + EncryptionContext encryption = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.ENCRYPT, KEY, spec); + ((ContextAware) encryption).setContext(context); + byte[] ciphertext; + try (InputStream stream = encryption.attach(new ByteArrayInputStream(plaintext))) { + ciphertext = stream.readAllBytes(); + } finally { + encryption.close(); + } + return new Encrypted(spec, context.get(ConfluxKeys.iv("AES")).clone(), ciphertext); + } + + private static byte[] decrypt(AesSpec spec, SecretKey key, byte[] iv, byte[] ciphertext) throws Exception { + CtxInterface context = newContext("aes-security-dec-"); + if (iv != null) { + context.put(ConfluxKeys.iv("AES"), iv); + } + EncryptionContext decryption = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.DECRYPT, key, spec); + ((ContextAware) decryption).setContext(context); + try (InputStream stream = decryption.attach(new ByteArrayInputStream(ciphertext))) { + return stream.readAllBytes(); + } finally { + decryption.close(); + } + } + + private static FailureResult failedDecryption(AesSpec spec, SecretKey key, byte[] iv, byte[] ciphertext) + throws Exception { + CtxInterface context = newContext("aes-security-fail-"); + context.put(ConfluxKeys.iv("AES"), iv); + EncryptionContext decryption = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.DECRYPT, key, spec); + ((ContextAware) decryption).setContext(context); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (InputStream stream = decryption.attach(new ByteArrayInputStream(ciphertext))) { + byte[] buffer = new byte[17]; + IOException failure = assertThrows(IOException.class, () -> { + int count; + while ((count = stream.read(buffer)) >= 0) { + if (count > 0) { + output.write(buffer, 0, count); + } + } + }); + return new FailureResult(output.size(), failure); + } finally { + decryption.close(); + } + } + + private static void assertAuthenticationFailure(IOException failure) { + assertTrue(hasCause(failure, BadPaddingException.class), + "GCM failure must retain an authentication-related provider cause"); + } + + private static boolean hasCause(Throwable failure, Class expectedType) { + for (Throwable cause = failure; cause != null; cause = cause.getCause()) { + if (expectedType.isInstance(cause)) { + return true; + } + } + return false; + } + + private static CtxInterface newContext(String prefix) { + return Ctx.INSTANCE.getContext(prefix + CONTEXT_IDS.incrementAndGet()); + } + + private static byte[] changed(byte[] input, int index) { + byte[] result = input.clone(); + result[index] ^= 0x01; + return result; + } + + private static byte[] removed(byte[] input, int index) { + byte[] result = new byte[input.length - 1]; + System.arraycopy(input, 0, result, 0, index); + System.arraycopy(input, index + 1, result, index, input.length - index - 1); + return result; + } + + private static byte[] repeated(byte value, int length) { + byte[] result = new byte[length]; + Arrays.fill(result, value); + return result; + } + + private record Encrypted(AesSpec spec, byte[] iv, byte[] ciphertext) {} + + private record FailureResult(int outputBytes, IOException failure) {} +} diff --git a/lib/src/test/java/zeroecho/core/alg/aes/AesGcmCrossCheckTest.java b/lib/src/test/java/zeroecho/core/alg/aes/AesGcmCrossCheckTest.java index b55d82e..2f79c8f 100644 --- a/lib/src/test/java/zeroecho/core/alg/aes/AesGcmCrossCheckTest.java +++ b/lib/src/test/java/zeroecho/core/alg/aes/AesGcmCrossCheckTest.java @@ -133,7 +133,7 @@ public class AesGcmCrossCheckTest { // --- key (either via your builder or direct JCA; both fine) --- CryptoAlgorithm aesAlg = CryptoAlgorithms.require("AES"); - SecretKey key = aesAlg.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256()); + SecretKey key = aesAlg.symmetricKeyGenerator(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256()); // or: // KeyGenerator kg = KeyGenerator.getInstance("AES"); // kg.init(256); @@ -148,7 +148,7 @@ public class AesGcmCrossCheckTest { AesSpec spec = AesSpec.gcm128(null); // === STREAM ENCRYPT === - EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec); + EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.ENCRYPT, key, spec); ((ContextAware) enc).setContext(session); byte[] ct_stream = readAll(enc.attach(new ByteArrayInputStream(msg))); enc.close(); @@ -160,7 +160,7 @@ public class AesGcmCrossCheckTest { assertArrayEquals(ct_jca, ct_stream, "STREAM ciphertext != JCA ciphertext (IV/AAD/msg must match)"); // === STREAM DECRYPT of JCA ciphertext === - EncryptionContext dec1 = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, key, spec); + EncryptionContext dec1 = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.DECRYPT, key, spec); ((ContextAware) dec1).setContext(session); // same IV/AAD in ctx byte[] pt1 = readAll(dec1.attach(new ByteArrayInputStream(ct_jca))); dec1.close(); diff --git a/lib/src/test/java/zeroecho/core/alg/aes/AesLargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/aes/AesLargeDataTest.java index 1119a88..ea95ef9 100644 --- a/lib/src/test/java/zeroecho/core/alg/aes/AesLargeDataTest.java +++ b/lib/src/test/java/zeroecho/core/alg/aes/AesLargeDataTest.java @@ -109,18 +109,18 @@ public class AesLargeDataTest { byte[] msg = randomBytes(SIZE); CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); - SecretKey key = aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256()); + SecretKey key = aes.symmetricKeyGenerator(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256()); CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime()); AesSpec spec = AesSpec.gcm128(null); - EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec); + EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.ENCRYPT, key, spec); ((ContextAware) enc).setContext(session); byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); enc.close(); - EncryptionContext dec = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, key, spec); + EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.DECRYPT, key, spec); ((ContextAware) dec).setContext(session); byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); dec.close(); @@ -139,18 +139,18 @@ public class AesLargeDataTest { System.out.printf("...input: %d bytes%n", msg.length); CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); - SecretKey key = aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256()); + SecretKey key = aes.symmetricKeyGenerator(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256()); CtxInterface session = Ctx.INSTANCE.getContext("aes-hdr-" + System.nanoTime()); AesSpec spec = AesSpec.gcm128(new AesHeaderCodec()); - EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec); + EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.ENCRYPT, key, spec); ((ContextAware) enc).setContext(session); byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); enc.close(); System.out.printf("...encrypted: %d bytes%n", ct.length); - EncryptionContext dec = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, key, spec); + EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.DECRYPT, key, spec); ((ContextAware) dec).setContext(session); byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); dec.close(); @@ -169,18 +169,18 @@ public class AesLargeDataTest { byte[] msg = randomBytes(SIZE); CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); - SecretKey key = aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256()); + SecretKey key = aes.symmetricKeyGenerator(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256()); CtxInterface session = Ctx.INSTANCE.getContext("aes-cbc-" + System.nanoTime()); AesSpec spec = AesSpec.cbcPkcs7(null); - EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec); + EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.ENCRYPT, key, spec); ((ContextAware) enc).setContext(session); byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); enc.close(); - EncryptionContext dec = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, key, spec); + EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("AES", KeyUsage.DECRYPT, key, spec); ((ContextAware) dec).setContext(session); byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); dec.close(); diff --git a/lib/src/test/java/zeroecho/core/alg/aes/AesRandomSupportTest.java b/lib/src/test/java/zeroecho/core/alg/aes/AesRandomSupportTest.java new file mode 100644 index 0000000..3d55c6f --- /dev/null +++ b/lib/src/test/java/zeroecho/core/alg/aes/AesRandomSupportTest.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core.alg.aes; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.io.ByteArrayInputStream; +import java.lang.reflect.Field; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.junit.jupiter.api.Test; + +import conflux.Ctx; +import conflux.CtxInterface; +import zeroecho.core.ConfluxKeys; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.spec.VoidSpec; +import zeroecho.core.spi.ContextAware; +import zeroecho.sdk.util.RandomSupport; + +class AesRandomSupportTest { + private static final SecretKey KEY = new SecretKeySpec(new byte[16], "AES"); + + @Test + void allFactoriesUseSharedRandomSource() throws Exception { + System.out.print("AesRandomSupport/allFactoriesUseSharedRandomSource..."); + AesAlgorithm algorithm = new AesAlgorithm(); + + assertSharedRandom(algorithm.createContext(KeyUsage.ENCRYPT, KEY, AesSpec.gcm128(null))); + assertSharedRandom(algorithm.createContext(KeyUsage.DECRYPT, KEY, AesSpec.gcm128(null))); + assertSharedRandom(algorithm.createContext(KeyUsage.ENCRYPT, KEY, VoidSpec.INSTANCE)); + assertSharedRandom(algorithm.createContext(KeyUsage.DECRYPT, KEY, VoidSpec.INSTANCE)); + + System.out.println("ok"); + } + + @Test + void nullFallbackUsesSharedRandomSource() throws Exception { + System.out.print("AesRandomSupport/nullFallbackUsesSharedRandomSource..."); + AesCipherContext context = new AesCipherContext(new AesAlgorithm(), KEY, true, AesSpec.gcm128(null), null); + + assertSharedRandom(context); + + System.out.println("ok"); + } + + @Test + void keyGenerationUsesEstablishedRandomSourcePath() throws Exception { + System.out.print("AesRandomSupport/keyGenerationUsesEstablishedRandomSourcePath..."); + AesAlgorithm algorithm = new AesAlgorithm(); + SecretKey generated = algorithm.symmetricKeyGenerator(AesKeyGenSpec.class) + .generateSecret(AesKeyGenSpec.aes128()); + + assertNotNull(generated); + assertEquals("AES", generated.getAlgorithm()); + assertEquals(16, generated.getEncoded().length); + System.out.println("...keyBytes=" + generated.getEncoded().length); + System.out.println("ok"); + } + + @Test + void controlledRandomProducesFreshIvForEachDirectContext() throws Exception { + System.out.print("AesRandomSupport/controlledRandomProducesFreshIvForEachDirectContext..."); + CountingSecureRandom random = new CountingSecureRandom(); + AesAlgorithm algorithm = new AesAlgorithm(); + CtxInterface firstSession = Ctx.INSTANCE.getContext("aes-controlled-first"); + CtxInterface secondSession = Ctx.INSTANCE.getContext("aes-controlled-second"); + + attachEmpty(new AesCipherContext(algorithm, KEY, true, AesSpec.gcm128(null), random), firstSession); + attachEmpty(new AesCipherContext(algorithm, KEY, true, AesSpec.gcm128(null), random), secondSession); + + byte[] firstIv = firstSession.get(ConfluxKeys.iv("AES")); + byte[] secondIv = secondSession.get(ConfluxKeys.iv("AES")); + assertArrayEquals(repeated((byte) 1, 12), firstIv); + assertArrayEquals(repeated((byte) 2, 12), secondIv); + assertNotSame(firstIv, secondIv); + System.out.println("...randomCalls=" + random.calls); + System.out.println("ok"); + } + + @Test + void concurrentFactoryConstructionUsesEstablishedSharedSource() throws Exception { + System.out.print("AesRandomSupport/concurrentFactoryConstructionUsesEstablishedSharedSource..."); + AesAlgorithm algorithm = new AesAlgorithm(); + ExecutorService executor = Executors.newFixedThreadPool(8); + try { + List> tasks = new ArrayList<>(); + for (int index = 0; index < 64; index++) { + tasks.add(() -> randomOf(algorithm.createContext(KeyUsage.ENCRYPT, KEY, AesSpec.gcm128(null)))); + } + for (java.util.concurrent.Future result : executor.invokeAll(tasks)) { + assertSame(RandomSupport.getRandom(), result.get()); + } + } finally { + executor.close(); + } + + System.out.println("ok"); + } + + private static void assertSharedRandom(EncryptionContext context) throws Exception { + assertSame(RandomSupport.getRandom(), randomOf(context)); + } + + private static SecureRandom randomOf(Object context) throws Exception { + Field field = AesCipherContext.class.getDeclaredField("rnd"); + field.setAccessible(true); + return (SecureRandom) field.get(context); + } + + private static void attachEmpty(AesCipherContext context, CtxInterface session) throws Exception { + ((ContextAware) context).setContext(session); + try (java.io.InputStream stream = context.attach(new ByteArrayInputStream(new byte[0]))) { + stream.readAllBytes(); + } + } + + private static byte[] repeated(byte value, int length) { + byte[] result = new byte[length]; + java.util.Arrays.fill(result, value); + return result; + } + + private static final class CountingSecureRandom extends SecureRandom { + private static final long serialVersionUID = 1L; + private int calls; + + @Override + public void nextBytes(byte[] bytes) { + calls++; + java.util.Arrays.fill(bytes, (byte) calls); + } + } +} diff --git a/lib/src/test/java/zeroecho/core/alg/chacha/ChaChaLargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/chacha/ChaChaLargeDataTest.java index 0fd713f..1181c8c 100644 --- a/lib/src/test/java/zeroecho/core/alg/chacha/ChaChaLargeDataTest.java +++ b/lib/src/test/java/zeroecho/core/alg/chacha/ChaChaLargeDataTest.java @@ -116,17 +116,17 @@ public class ChaChaLargeDataTest { byte[] msg = randomBytes(SIZE); CryptoAlgorithm chacha = CryptoAlgorithms.require("CHACHA20"); - SecretKey key = chacha.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); + SecretKey key = chacha.symmetricKeyGenerator(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); CtxInterface session = Ctx.INSTANCE.getContext("chacha-ctx-" + System.nanoTime()); ChaChaSpec spec = ChaChaSpec.builder().initialCounter(1).header(null).build(); - EncryptionContext enc = CryptoAlgorithms.create("CHACHA20", KeyUsage.ENCRYPT, key, spec); + EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20", KeyUsage.ENCRYPT, key, spec); ((ContextAware) enc).setContext(session); byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); enc.close(); - EncryptionContext dec = CryptoAlgorithms.create("CHACHA20", KeyUsage.DECRYPT, key, spec); + EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20", KeyUsage.DECRYPT, key, spec); ((ContextAware) dec).setContext(session); byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); dec.close(); @@ -143,17 +143,17 @@ public class ChaChaLargeDataTest { byte[] msg = randomBytes(SIZE); CryptoAlgorithm chacha = CryptoAlgorithms.require("CHACHA20"); - SecretKey key = chacha.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); + SecretKey key = chacha.symmetricKeyGenerator(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); CtxInterface session = Ctx.INSTANCE.getContext("chacha-hdr-" + System.nanoTime()); ChaChaSpec spec = ChaChaSpec.builder().initialCounter(1).header(new ChaChaHeaderCodec()).build(); - EncryptionContext enc = CryptoAlgorithms.create("CHACHA20", KeyUsage.ENCRYPT, key, spec); + EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20", KeyUsage.ENCRYPT, key, spec); ((ContextAware) enc).setContext(session); byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); enc.close(); - EncryptionContext dec = CryptoAlgorithms.create("CHACHA20", KeyUsage.DECRYPT, key, spec); + EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20", KeyUsage.DECRYPT, key, spec); ((ContextAware) dec).setContext(session); byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); dec.close(); @@ -174,14 +174,14 @@ public class ChaChaLargeDataTest { byte[] msg = randomBytes(SIZE); CryptoAlgorithm chacha = CryptoAlgorithms.require("CHACHA20"); - SecretKey key = chacha.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); + SecretKey key = chacha.symmetricKeyGenerator(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); // Encrypt with explicit counter=7 (in ctx), headerless (ctx-only). CtxInterface encCtx = Ctx.INSTANCE.getContext("chacha-ctr-enc-" + System.nanoTime()); encCtx.put(ConfluxKeys.tagBits("CHACHA20"), 7); ChaChaSpec spec = ChaChaSpec.builder().initialCounter(1).header(null).build(); - EncryptionContext enc = CryptoAlgorithms.create("CHACHA20", KeyUsage.ENCRYPT, key, spec); + EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20", KeyUsage.ENCRYPT, key, spec); ((ContextAware) enc).setContext(encCtx); byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); enc.close(); @@ -194,7 +194,7 @@ public class ChaChaLargeDataTest { decCtxOk.put(ConfluxKeys.iv("CHACHA20"), nonce); decCtxOk.put(ConfluxKeys.tagBits("CHACHA20"), 7); - EncryptionContext decOk = CryptoAlgorithms.create("CHACHA20", KeyUsage.DECRYPT, key, spec); + EncryptionContext decOk = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20", KeyUsage.DECRYPT, key, spec); ((ContextAware) decOk).setContext(decCtxOk); byte[] ptOk = readAll(decOk.attach(new ByteArrayInputStream(ct))); decOk.close(); @@ -205,7 +205,7 @@ public class ChaChaLargeDataTest { decCtxBad.put(ConfluxKeys.iv("CHACHA20"), nonce); decCtxBad.put(ConfluxKeys.tagBits("CHACHA20"), 8); - EncryptionContext decBad = CryptoAlgorithms.create("CHACHA20", KeyUsage.DECRYPT, key, spec); + EncryptionContext decBad = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20", KeyUsage.DECRYPT, key, spec); ((ContextAware) decBad).setContext(decCtxBad); byte[] ptBad = readAll(decBad.attach(new ByteArrayInputStream(ct))); decBad.close(); @@ -228,19 +228,19 @@ public class ChaChaLargeDataTest { byte[] aad = "associated-data-ctx-only".getBytes(); CryptoAlgorithm aead = CryptoAlgorithms.require("CHACHA20-POLY1305"); - SecretKey key = aead.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); + SecretKey key = aead.symmetricKeyGenerator(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); CtxInterface session = Ctx.INSTANCE.getContext("chacha-aead-ctx-" + System.nanoTime()); session.put(ConfluxKeys.aad("CHACHA20-POLY1305"), aad); ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(null).build(); - EncryptionContext enc = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.ENCRYPT, key, spec); + EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20-POLY1305", KeyUsage.ENCRYPT, key, spec); ((ContextAware) enc).setContext(session); byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); enc.close(); - EncryptionContext dec = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.DECRYPT, key, spec); + EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20-POLY1305", KeyUsage.DECRYPT, key, spec); ((ContextAware) dec).setContext(session); byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); dec.close(); @@ -259,19 +259,19 @@ public class ChaChaLargeDataTest { byte[] aad = "associated-data-header".getBytes(); CryptoAlgorithm aead = CryptoAlgorithms.require("CHACHA20-POLY1305"); - SecretKey key = aead.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); + SecretKey key = aead.symmetricKeyGenerator(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); CtxInterface session = Ctx.INSTANCE.getContext("chacha-aead-hdr-" + System.nanoTime()); session.put(ConfluxKeys.aad("CHACHA20-POLY1305"), aad); ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(new ChaCha20Poly1305HeaderCodec()).build(); - EncryptionContext enc = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.ENCRYPT, key, spec); + EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20-POLY1305", KeyUsage.ENCRYPT, key, spec); ((ContextAware) enc).setContext(session); byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); enc.close(); - EncryptionContext dec = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.DECRYPT, key, spec); + EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20-POLY1305", KeyUsage.DECRYPT, key, spec); ((ContextAware) dec).setContext(session); byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); dec.close(); @@ -294,7 +294,7 @@ public class ChaChaLargeDataTest { byte[] aadDec = "aad-dec-different".getBytes(); CryptoAlgorithm aead = CryptoAlgorithms.require("CHACHA20-POLY1305"); - SecretKey key = aead.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); + SecretKey key = aead.symmetricKeyGenerator(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); // Encrypt with AAD = aadEnc (ctx-only, no header) CtxInterface encCtx = Ctx.INSTANCE.getContext("chacha-aead-enc-" + System.nanoTime()); @@ -302,7 +302,7 @@ public class ChaChaLargeDataTest { ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(null).build(); - EncryptionContext enc = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.ENCRYPT, key, spec); + EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20-POLY1305", KeyUsage.ENCRYPT, key, spec); ((ContextAware) enc).setContext(encCtx); byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); enc.close(); @@ -314,7 +314,7 @@ public class ChaChaLargeDataTest { byte[] nonce = encCtx.get(ConfluxKeys.iv("CHACHA20-POLY1305")); decCtx.put(ConfluxKeys.iv("CHACHA20-POLY1305"), nonce); - EncryptionContext dec = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.DECRYPT, key, spec); + EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("CHACHA20-POLY1305", KeyUsage.DECRYPT, key, spec); ((ContextAware) dec).setContext(decCtx); assertThrows(IOException.class, () -> { diff --git a/lib/src/test/java/zeroecho/core/alg/common/agreement/AgreementAlgorithmsRoundTripTest.java b/lib/src/test/java/zeroecho/core/alg/common/agreement/AgreementAlgorithmsRoundTripTest.java index 7b3db45..cfc7e33 100644 --- a/lib/src/test/java/zeroecho/core/alg/common/agreement/AgreementAlgorithmsRoundTripTest.java +++ b/lib/src/test/java/zeroecho/core/alg/common/agreement/AgreementAlgorithmsRoundTripTest.java @@ -62,7 +62,9 @@ import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.ContextSpec; import zeroecho.core.spec.VoidSpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.KeyOperation; +import zeroecho.core.KeyOperationInfo; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; import zeroecho.sdk.util.BouncyCastleActivator; public class AgreementAlgorithmsRoundTripTest { @@ -174,7 +176,7 @@ public class AgreementAlgorithmsRoundTripTest { ContextSpec spec = null; try { - spec = cap.defaultSpec().get(); + spec = cap.defaultSpec(); } catch (Throwable ignore) { spec = tryExtractContextSpec(alg); } @@ -204,8 +206,8 @@ public class AgreementAlgorithmsRoundTripTest { MessageAgreementContext bCtx = null; try { - aCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, aliceKey, spec); - bCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bobKey, spec); + aCtx = new zeroecho.sdk.ZeroEchoSession().createContext(alg.id(), KeyUsage.AGREEMENT, aliceKey, spec); + bCtx = new zeroecho.sdk.ZeroEchoSession().createContext(alg.id(), KeyUsage.AGREEMENT, bobKey, spec); byte[] aMsg = aCtx.getPeerMessage(); byte[] bMsg = bCtx.getPeerMessage(); @@ -272,11 +274,11 @@ public class AgreementAlgorithmsRoundTripTest { try { // Alice (initiator): has Bob's public key - aliceCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPublic(), + aliceCtx = new zeroecho.sdk.ZeroEchoSession().createContext(alg.id(), KeyUsage.AGREEMENT, bob.getPublic(), VoidSpec.INSTANCE); // Bob (responder): has his private key - bobCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(), + bobCtx = new zeroecho.sdk.ZeroEchoSession().createContext(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(), VoidSpec.INSTANCE); // Initiator produces encapsulation message (ciphertext) to send @@ -324,7 +326,7 @@ public class AgreementAlgorithmsRoundTripTest { ContextSpec spec = null; try { - spec = cap.defaultSpec().get(); + spec = cap.defaultSpec(); } catch (Throwable ignore) { spec = tryExtractContextSpec(alg); } @@ -360,8 +362,8 @@ public class AgreementAlgorithmsRoundTripTest { AgreementContext bCtx = null; try { - aCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, alice.getPrivate(), spec); - bCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(), spec); + aCtx = new zeroecho.sdk.ZeroEchoSession().createContext(alg.id(), KeyUsage.AGREEMENT, alice.getPrivate(), spec); + bCtx = new zeroecho.sdk.ZeroEchoSession().createContext(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(), spec); aCtx.setPeerPublic(bob.getPublic()); bCtx.setPeerPublic(alice.getPublic()); @@ -402,14 +404,14 @@ public class AgreementAlgorithmsRoundTripTest { // ----- helpers ----- private static KeyPair generateKeyPair(CryptoAlgorithm alg) { try { - for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) { - if (bi.defaultKeySpec == null) { + for (KeyOperationInfo bi : alg.keyOperations()) { + if (bi.operation() != KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE || bi.defaultSpec() == null) { continue; } @SuppressWarnings("unchecked") - Class specType = (Class) bi.specType; - AsymmetricKeyBuilder builder = alg.asymmetricKeyBuilder(specType); - AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec; + Class specType = (Class) bi.specType(); + AsymmetricKeyPairGenerator builder = alg.asymmetricKeyPairGenerator(specType); + AlgorithmKeySpec spec = bi.defaultSpec(); return builder.generateKeyPair(spec); } } catch (Throwable ignore) { @@ -423,7 +425,7 @@ public class AgreementAlgorithmsRoundTripTest { for (Capability c : alg.listCapabilities()) { if (c.role() == KeyUsage.AGREEMENT && ContextSpec.class.isAssignableFrom(c.specType())) { try { - return c.defaultSpec().get(); + return c.defaultSpec(); } catch (Throwable ignore) { // continue searching } diff --git a/lib/src/test/java/zeroecho/core/alg/ecdsa/EcdsaLargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/ecdsa/EcdsaLargeDataTest.java index 92c1a9e..b6cf953 100644 --- a/lib/src/test/java/zeroecho/core/alg/ecdsa/EcdsaLargeDataTest.java +++ b/lib/src/test/java/zeroecho/core/alg/ecdsa/EcdsaLargeDataTest.java @@ -132,10 +132,10 @@ public class EcdsaLargeDataTest { System.out.println("...msg=" + msg.length + " bytes"); // Key pair via your unified ECDSA algorithm and enum spec - KeyPair kp = CryptoAlgorithms.keyPair("ECDSA", spec); + KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ECDSA", spec); // SIGN (streaming): emits [body][signature]; capture trailer - SignatureContext signer = CryptoAlgorithms.create("ECDSA", KeyUsage.SIGN, kp.getPrivate(), spec); + SignatureContext signer = new zeroecho.sdk.ZeroEchoSession().createContext("ECDSA", KeyUsage.SIGN, kp.getPrivate(), spec); final byte[][] sigHolder = new byte[1][]; final int sigLen = signer.tagLength(); // should equal spec.signFixedLength() @@ -160,7 +160,7 @@ public class EcdsaLargeDataTest { System.out.println("...signature size: " + ourSig.length + " (expected " + spec.signFixedLength() + ")"); // VERIFY with our streaming verifier - SignatureContext verifier = CryptoAlgorithms.create("ECDSA", KeyUsage.VERIFY, kp.getPublic(), spec); + SignatureContext verifier = new zeroecho.sdk.ZeroEchoSession().createContext("ECDSA", KeyUsage.VERIFY, kp.getPublic(), spec); verifier.setExpectedTag(ourSig); byte[] sink2; try (InputStream verIn = verifier.wrap(new ByteArrayInputStream(msg))) { @@ -181,7 +181,7 @@ public class EcdsaLargeDataTest { // Extra symmetry check (optional): our verifier must accept a JCA signature // (different bytes) byte[] jcaSig = jcaEcdsaSign(spec.jcaFactory(), kp.getPrivate(), msg); - SignatureContext verifier2 = CryptoAlgorithms.create("ECDSA", KeyUsage.VERIFY, kp.getPublic(), spec); + SignatureContext verifier2 = new zeroecho.sdk.ZeroEchoSession().createContext("ECDSA", KeyUsage.VERIFY, kp.getPublic(), spec); verifier2.setExpectedTag(jcaSig); try (InputStream verIn2 = verifier2.wrap(new ByteArrayInputStream(msg))) { byte[] passthrough = readAll(verIn2); diff --git a/lib/src/test/java/zeroecho/core/alg/ed25519/Ed25519LargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/ed25519/Ed25519LargeDataTest.java index ce3a279..729fc94 100644 --- a/lib/src/test/java/zeroecho/core/alg/ed25519/Ed25519LargeDataTest.java +++ b/lib/src/test/java/zeroecho/core/alg/ed25519/Ed25519LargeDataTest.java @@ -132,11 +132,11 @@ public class Ed25519LargeDataTest { return; } - KeyPair kp = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec()); + KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec()); // SIGN: context emits [body][signature] — capture trailer via // TailStrippingInputStream - SignatureContext signer = CryptoAlgorithms.create("Ed25519", KeyUsage.SIGN, kp.getPrivate(), null); + SignatureContext signer = new zeroecho.sdk.ZeroEchoSession().createContext("Ed25519", KeyUsage.SIGN, kp.getPrivate(), null); final byte[][] sigHolder = new byte[1][]; final int sigLen = signer.tagLength(); @@ -166,7 +166,7 @@ public class Ed25519LargeDataTest { assertArrayEquals(refSig, ourSig, "signature mismatch vs JCA reference"); // VERIFY: supply expected tag and drain (throws on mismatch) - SignatureContext verifier = CryptoAlgorithms.create("Ed25519", KeyUsage.VERIFY, kp.getPublic(), null); + SignatureContext verifier = new zeroecho.sdk.ZeroEchoSession().createContext("Ed25519", KeyUsage.VERIFY, kp.getPublic(), null); verifier.setExpectedTag(ourSig); byte[] sink2; diff --git a/lib/src/test/java/zeroecho/core/alg/ed448/Ed448LargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/ed448/Ed448LargeDataTest.java index b7dafb9..28d515f 100644 --- a/lib/src/test/java/zeroecho/core/alg/ed448/Ed448LargeDataTest.java +++ b/lib/src/test/java/zeroecho/core/alg/ed448/Ed448LargeDataTest.java @@ -132,11 +132,11 @@ public class Ed448LargeDataTest { return; } - KeyPair kp = CryptoAlgorithms.keyPair("Ed448", Ed448KeyGenSpec.defaultSpec()); + KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Ed448", Ed448KeyGenSpec.defaultSpec()); // SIGN: context emits [body][signature] — capture trailer via // TailStrippingInputStream - SignatureContext signer = CryptoAlgorithms.create("Ed448", KeyUsage.SIGN, kp.getPrivate(), null); + SignatureContext signer = new zeroecho.sdk.ZeroEchoSession().createContext("Ed448", KeyUsage.SIGN, kp.getPrivate(), null); final byte[][] sigHolder = new byte[1][]; final int sigLen = signer.tagLength(); @@ -166,7 +166,7 @@ public class Ed448LargeDataTest { assertArrayEquals(refSig, ourSig, "signature mismatch vs JCA reference"); // VERIFY: supply expected tag and drain (throws on mismatch) - SignatureContext verifier = CryptoAlgorithms.create("Ed448", KeyUsage.VERIFY, kp.getPublic(), null); + SignatureContext verifier = new zeroecho.sdk.ZeroEchoSession().createContext("Ed448", KeyUsage.VERIFY, kp.getPublic(), null); verifier.setExpectedTag(ourSig); byte[] sink2; diff --git a/lib/src/test/java/zeroecho/core/alg/elgamal/ElgamalLargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/elgamal/ElgamalLargeDataTest.java index 3d5e683..24b88b4 100644 --- a/lib/src/test/java/zeroecho/core/alg/elgamal/ElgamalLargeDataTest.java +++ b/lib/src/test/java/zeroecho/core/alg/elgamal/ElgamalLargeDataTest.java @@ -93,16 +93,16 @@ public class ElgamalLargeDataTest { byte[] msg = randomBytes(SIZE); System.out.printf("...input: %d bytes%n", msg.length); - KeyPair kp = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); ElgamalEncSpec spec = ElgamalEncSpec.pkcs1(); - EncryptionContext enc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, kp.getPublic(), spec); + EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.ENCRYPT, kp.getPublic(), spec); InputStream ctIn = enc.attach(new ByteArrayInputStream(msg)); byte[] ct = ctIn.readAllBytes(); enc.close(); System.out.printf("...encrypted: %d bytes%n", ct.length); - EncryptionContext dec = CryptoAlgorithms.create("ElGamal", KeyUsage.DECRYPT, kp.getPrivate(), spec); + EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.DECRYPT, kp.getPrivate(), spec); InputStream ptIn = dec.attach(new ByteArrayInputStream(ct)); byte[] pt = ptIn.readAllBytes(); dec.close(); @@ -121,16 +121,16 @@ public class ElgamalLargeDataTest { byte[] msg = randomBytes(SIZE); System.out.printf("...input: %d bytes%n", msg.length); - KeyPair kp = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); ElgamalEncSpec spec = ElgamalEncSpec.noPadding(); - EncryptionContext enc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, kp.getPublic(), spec); + EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.ENCRYPT, kp.getPublic(), spec); InputStream ctIn = enc.attach(new ByteArrayInputStream(msg)); byte[] ct = ctIn.readAllBytes(); enc.close(); System.out.printf("...encrypted: %d bytes%n", ct.length); - EncryptionContext dec = CryptoAlgorithms.create("ElGamal", KeyUsage.DECRYPT, kp.getPrivate(), spec); + EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.DECRYPT, kp.getPrivate(), spec); InputStream ptIn = dec.attach(new ByteArrayInputStream(ct)); byte[] pt = ptIn.readAllBytes(); dec.close(); @@ -149,10 +149,10 @@ public class ElgamalLargeDataTest { byte[] msg = randomBytes(SIZE); System.out.printf("...input: %d bytes%n", msg.length); - KeyPair kp = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); ElgamalEncSpec spec = ElgamalEncSpec.noPadding(); - EncryptionContext enc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, kp.getPublic(), spec); + EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.ENCRYPT, kp.getPublic(), spec); InputStream ctIn = enc.attach(new ByteArrayInputStream(msg)); assertThrowsExactly(IllegalStateException.class, () -> ctIn.readAllBytes(), "No-padding cipher streams cannot processes incomplete blocks: 3 instead of 255"); diff --git a/lib/src/test/java/zeroecho/core/alg/hmac/HmacLargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/hmac/HmacLargeDataTest.java index 5f2bb6b..d9619b4 100644 --- a/lib/src/test/java/zeroecho/core/alg/hmac/HmacLargeDataTest.java +++ b/lib/src/test/java/zeroecho/core/alg/hmac/HmacLargeDataTest.java @@ -127,11 +127,11 @@ public class HmacLargeDataTest { CryptoAlgorithm algo = CryptoAlgorithms.require(ALG_ID); // Generate a key (macName must match) - SecretKey key = algo.symmetricKeyBuilder(HmacKeyGenSpec.class).generateSecret(new HmacKeyGenSpec(JCA_MAC, 256)); + SecretKey key = algo.symmetricKeyGenerator(HmacKeyGenSpec.class).generateSecret(new HmacKeyGenSpec(JCA_MAC, 256)); // --- MAC (produce): engine emits [body][tag]; capture trailer --- HmacSpec spec = HmacSpec.sha256(); - MacContext mac = CryptoAlgorithms.create(ALG_ID, KeyUsage.MAC, key, spec); + MacContext mac = new zeroecho.sdk.ZeroEchoSession().createContext(ALG_ID, KeyUsage.MAC, key, spec); final byte[][] tagHolder = new byte[1][]; final int tagLen = mac.tagLength(); @@ -161,7 +161,7 @@ public class HmacLargeDataTest { assertArrayEquals(ref, tag, "HMAC tag mismatch vs JCA reference"); // --- VERIFY (consume): provide expected tag and drain (throws on mismatch) --- - MacContext ver = CryptoAlgorithms.create(ALG_ID, KeyUsage.MAC, key, spec); + MacContext ver = new zeroecho.sdk.ZeroEchoSession().createContext(ALG_ID, KeyUsage.MAC, key, spec); ver.setExpectedTag(tag); byte[] pass2; try (InputStream in = ver.wrap(new ByteArrayInputStream(msg))) { diff --git a/lib/src/test/java/zeroecho/core/alg/mldsa/MldsaLargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/mldsa/MldsaLargeDataTest.java index ecb6ae2..b50b048 100644 --- a/lib/src/test/java/zeroecho/core/alg/mldsa/MldsaLargeDataTest.java +++ b/lib/src/test/java/zeroecho/core/alg/mldsa/MldsaLargeDataTest.java @@ -117,13 +117,13 @@ public final class MldsaLargeDataTest { String caseId = "ML-DSA " + ps.name() + " preHash=" + preHash.name(); System.out.println(INDENT + " case=" + safeText(caseId)); - KeyPair kp = CryptoAlgorithms.keyPair("ML-DSA", spec); + KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-DSA", spec); - SignatureContext mldsaVerifier = CryptoAlgorithms.create("ML-DSA", KeyUsage.VERIFY, kp.getPublic()); + SignatureContext mldsaVerifier = new zeroecho.sdk.ZeroEchoSession().createContext("ML-DSA", KeyUsage.VERIFY, kp.getPublic()); int expectedSigLen = mldsaVerifier.tagLength(); System.out.println(INDENT + " expectedSigLen=" + expectedSigLen); - SignatureContext signer = CryptoAlgorithms.create("ML-DSA", KeyUsage.SIGN, kp.getPrivate()); + SignatureContext signer = new zeroecho.sdk.ZeroEchoSession().createContext("ML-DSA", KeyUsage.SIGN, kp.getPrivate()); final byte[][] sigHolder = new byte[1][]; byte[] passthrough; @@ -178,7 +178,7 @@ public final class MldsaLargeDataTest { byte[] badSig = Arrays.copyOf(signature, signature.length); badSig[0] = (byte) (badSig[0] ^ 0x01); - SignatureContext badVerifier = CryptoAlgorithms.create("ML-DSA", KeyUsage.VERIFY, kp.getPublic()); + SignatureContext badVerifier = new zeroecho.sdk.ZeroEchoSession().createContext("ML-DSA", KeyUsage.VERIFY, kp.getPublic()); try { badVerifier.setExpectedTag(badSig); diff --git a/lib/src/test/java/zeroecho/core/alg/rsa/BlockGeometryTest.java b/lib/src/test/java/zeroecho/core/alg/rsa/BlockGeometryTest.java new file mode 100644 index 0000000..e7d90a9 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/alg/rsa/BlockGeometryTest.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core.alg.rsa; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.KeyPair; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.EncryptionContext; + +class BlockGeometryTest { + @Test + void geometryValidation() { + System.out.println("geometryValidation"); + assertThrows(IllegalArgumentException.class, () -> new BlockGeometry(1, 1, 0)); + assertThrows(IllegalArgumentException.class, () -> new BlockGeometry(2, 0, 0)); + assertThrows(IllegalArgumentException.class, () -> new BlockGeometry(3, 2, 0)); + assertThrows(IllegalArgumentException.class, () -> new BlockGeometry(2, 2, -1)); + assertThrows(IllegalArgumentException.class, () -> new BlockGeometry(2, 2, 1)); + + BlockGeometry maximum = new BlockGeometry(Integer.MAX_VALUE, Integer.MAX_VALUE, 0); + assertEquals(Integer.MAX_VALUE, maximum.inChunkSize()); + assertEquals(Integer.MAX_VALUE, maximum.outChunkSize()); + System.out.println("...maximum=" + maximum.inChunkSize()); + System.out.println("geometryValidation...ok"); + } + + @Test + void valueSemantics() { + System.out.println("valueSemantics"); + BlockGeometry first = new BlockGeometry(2, 3, 0); + BlockGeometry equal = new BlockGeometry(2, 3, 0); + BlockGeometry different = new BlockGeometry(2, 4, 0); + + assertEquals(first, equal); + assertEquals(first.hashCode(), equal.hashCode()); + assertNotEquals(first, different); + assertEquals(2, first.inChunkSize()); + assertEquals(3, first.outChunkSize()); + assertEquals(0, first.finalizationOutputChunks()); + assertEquals("BlockGeometry[inChunkSize=2, outChunkSize=3, finalizationOutputChunks=0]", + first.toString()); + System.out.println("...legacyFields=true"); + System.out.println("valueSemantics...ok"); + } + + @Test + void actualRsaPath() throws Exception { + System.out.println("actualRsaPath"); + KeyPair keyPair = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048()); + RsaEncSpec spec = RsaEncSpec.oaep(RsaEncSpec.Hash.SHA256); + BlockGeometry encryptGeometry = BlockGeometry.forRsa(spec, keyPair.getPublic(), true); + BlockGeometry decryptGeometry = BlockGeometry.forRsa(spec, keyPair.getPrivate(), false); + + assertEquals(190, encryptGeometry.inChunkSize()); + assertEquals(256, encryptGeometry.outChunkSize()); + assertEquals(256, decryptGeometry.inChunkSize()); + assertEquals(256, decryptGeometry.outChunkSize()); + + EncryptionContext context = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, keyPair.getPublic(), spec); + try (InputStream stream = context.attach(new ByteArrayInputStream(new byte[] { 1, 2, 3 }))) { + assertEquals(256, stream.readAllBytes().length); + } finally { + context.close(); + } + System.out.println("...ciphertextBytes=256"); + System.out.println("actualRsaPath...ok"); + } +} diff --git a/lib/src/test/java/zeroecho/core/alg/rsa/RsaLargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/rsa/RsaLargeDataTest.java index 1cb4e85..fa5da78 100644 --- a/lib/src/test/java/zeroecho/core/alg/rsa/RsaLargeDataTest.java +++ b/lib/src/test/java/zeroecho/core/alg/rsa/RsaLargeDataTest.java @@ -89,16 +89,16 @@ public class RsaLargeDataTest { byte[] msg = randomBytes(SIZE); System.out.printf("...input: %d bytes%n", msg.length); - KeyPair kp = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048()); + KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048()); RsaEncSpec spec = RsaEncSpec.oaep(RsaEncSpec.Hash.SHA256); - EncryptionContext enc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, kp.getPublic(), spec); + EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, kp.getPublic(), spec); InputStream ctIn = enc.attach(new ByteArrayInputStream(msg)); byte[] ct = ctIn.readAllBytes(); enc.close(); System.out.printf("...encrypted: %d bytes%n", ct.length); - EncryptionContext dec = CryptoAlgorithms.create("RSA", KeyUsage.DECRYPT, kp.getPrivate(), spec); + EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.DECRYPT, kp.getPrivate(), spec); InputStream ptIn = dec.attach(new ByteArrayInputStream(ct)); byte[] pt = ptIn.readAllBytes(); dec.close(); @@ -117,16 +117,16 @@ public class RsaLargeDataTest { byte[] msg = randomBytes(SIZE); System.out.printf("...input: %d bytes%n", msg.length); - KeyPair kp = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048()); + KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048()); RsaEncSpec spec = RsaEncSpec.pkcs1v15(); - EncryptionContext enc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, kp.getPublic(), spec); + EncryptionContext enc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, kp.getPublic(), spec); InputStream ctIn = enc.attach(new ByteArrayInputStream(msg)); byte[] ct = ctIn.readAllBytes(); enc.close(); System.out.printf("...encrypted: %d bytes%n", ct.length); - EncryptionContext dec = CryptoAlgorithms.create("RSA", KeyUsage.DECRYPT, kp.getPrivate(), spec); + EncryptionContext dec = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.DECRYPT, kp.getPrivate(), spec); InputStream ptIn = dec.attach(new ByteArrayInputStream(ct)); byte[] pt = ptIn.readAllBytes(); dec.close(); diff --git a/lib/src/test/java/zeroecho/core/alg/slhdsa/SlhDsaLargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/slhdsa/SlhDsaLargeDataTest.java index 6036983..64763f0 100644 --- a/lib/src/test/java/zeroecho/core/alg/slhdsa/SlhDsaLargeDataTest.java +++ b/lib/src/test/java/zeroecho/core/alg/slhdsa/SlhDsaLargeDataTest.java @@ -122,18 +122,18 @@ public final class SlhDsaLargeDataTest { String caseId = "SLH-DSA " + hash.name() + " " + sec.name() + " " + variant.name(); System.out.println(INDENT + " case=" + safeText(caseId)); - KeyPair kp = CryptoAlgorithms.keyPair("SLH-DSA", spec); + KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("SLH-DSA", spec); // Create verifier FIRST to obtain tag length via // SlhDsaSignatureContext.sigLenFromPublicKey. - SignatureContext verifierCtx = CryptoAlgorithms.create("SLH-DSA", KeyUsage.VERIFY, kp.getPublic()); + SignatureContext verifierCtx = new zeroecho.sdk.ZeroEchoSession().createContext("SLH-DSA", KeyUsage.VERIFY, kp.getPublic()); int expectedSigLen = verifierCtx.tagLength(); System.out.println(INDENT + " expectedSigLen=" + expectedSigLen); // Now sign and strip trailer using the expected length from verifier (not from // signer). - SignatureContext signer = CryptoAlgorithms.create("SLH-DSA", KeyUsage.SIGN, kp.getPrivate()); + SignatureContext signer = new zeroecho.sdk.ZeroEchoSession().createContext("SLH-DSA", KeyUsage.SIGN, kp.getPrivate()); final byte[][] sigHolder = new byte[1][]; byte[] passthrough; @@ -188,7 +188,7 @@ public final class SlhDsaLargeDataTest { byte[] badSig = Arrays.copyOf(signature, signature.length); badSig[0] = (byte) (badSig[0] ^ 0x01); - SignatureContext badVerifier = CryptoAlgorithms.create("SLH-DSA", KeyUsage.VERIFY, kp.getPublic()); + SignatureContext badVerifier = new zeroecho.sdk.ZeroEchoSession().createContext("SLH-DSA", KeyUsage.VERIFY, kp.getPublic()); try { badVerifier.setExpectedTag(badSig); diff --git a/lib/src/test/java/zeroecho/core/audit/AuditedContextsAccessorTest.java b/lib/src/test/java/zeroecho/core/audit/AuditedContextsAccessorTest.java new file mode 100644 index 0000000..7a24d9c --- /dev/null +++ b/lib/src/test/java/zeroecho/core/audit/AuditedContextsAccessorTest.java @@ -0,0 +1,272 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core.audit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.Key; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.KeyUsage; +import zeroecho.core.NullKey; +import zeroecho.core.alg.aes.AesAlgorithm; +import zeroecho.core.context.CryptoContext; +import zeroecho.core.context.DigestContext; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.spec.ContextSpec; +import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate; + +public class AuditedContextsAccessorTest { + @Test + void specAccessorBinding() { + System.out.println("specAccessorBinding"); + MetadataListener firstListener = new MetadataListener(); + MetadataListener secondListener = new MetadataListener(); + MetadataListener missingListener = new MetadataListener(); + MetadataListener incompatibleListener = new MetadataListener(); + + AuditedContexts.wrap(new FirstSpecDigest(), firstListener, KeyUsage.DIGEST); + AuditedContexts.wrap(new SecondSpecDigest(), secondListener, KeyUsage.DIGEST); + AuditedContexts.wrap(new BaseDigest(), missingListener, KeyUsage.DIGEST); + AuditedContexts.wrap(new IncompatibleAccessorsContext(), incompatibleListener, KeyUsage.ENCRYPT); + + assertNotNull(firstListener.specMeta); + assertNotNull(secondListener.specMeta); + assertNull(missingListener.specMeta); + assertNull(incompatibleListener.specMeta); + assertEquals("default", firstListener.provider); + assertEquals("default", secondListener.provider); + System.out.println("...runtimeClasses=4"); + System.out.println("specAccessorBinding...ok"); + } + + @Test + void tagLengthAccessorBinding() throws Exception { + System.out.println("tagLengthAccessorBinding"); + BaseDigest explicitTarget = new BaseDigest(); + DigestContext explicit = (DigestContext) AuditedContexts.wrap(explicitTarget, AuditListener.noop(), + KeyUsage.DIGEST); + assertEquals(4, explicit.tagLength()); + assertEquals(4, explicit.tagLength()); + explicit.wrap(new ByteArrayInputStream(new byte[] { 1 })).readAllBytes(); + assertEquals(2, explicitTarget.tagCalls.get()); + + BaseDigest lazyTarget = new BaseDigest(); + DigestContext lazy = (DigestContext) AuditedContexts.wrap(lazyTarget, AuditListener.noop(), KeyUsage.DIGEST); + lazy.wrap(new ByteArrayInputStream(new byte[] { 1 })).readAllBytes(); + lazy.wrap(new ByteArrayInputStream(new byte[] { 2 })).readAllBytes(); + assertEquals(1, lazyTarget.tagCalls.get()); + + BaseDigest concurrentTarget = new BaseDigest(); + DigestContext concurrent = (DigestContext) AuditedContexts.wrap(concurrentTarget, AuditListener.noop(), + KeyUsage.DIGEST); + ExecutorService executor = Executors.newFixedThreadPool(8); + try { + List> calls = new ArrayList<>(); + for (int index = 0; index < 64; index++) { + calls.add(concurrent::tagLength); + } + for (java.util.concurrent.Future result : executor.invokeAll(calls)) { + assertEquals(4, result.get()); + } + } finally { + executor.close(); + } + assertEquals(64, concurrentTarget.tagCalls.get()); + System.out.println("...concurrentCalls=" + concurrentTarget.tagCalls.get()); + System.out.println("tagLengthAccessorBinding...ok"); + } + + @Test + void accessorFailureOrder() throws Exception { + System.out.println("accessorFailureOrder"); + RecordingListener listener = new RecordingListener(); + IllegalStateException expected = new IllegalStateException("controlled tag failure"); + DigestContext failing = (DigestContext) AuditedContexts.wrap(new FailingDigest(expected), listener, + KeyUsage.DIGEST); + + IllegalStateException actual = assertThrows(IllegalStateException.class, failing::tagLength); + assertSame(expected, actual); + assertSame(expected, listener.failure); + assertEquals(List.of("meta", "failure"), listener.events); + + RecordingListener successListener = new RecordingListener(); + DigestContext successful = (DigestContext) AuditedContexts.wrap(new BaseDigest(), successListener, + KeyUsage.DIGEST); + successful.wrap(new ByteArrayInputStream(new byte[] { 1, 2, 3 })).readAllBytes(); + assertEquals("meta", successListener.events.get(0)); + assertEquals("tag", successListener.events.get(successListener.events.size() - 1)); + System.out.println("...failureEvents=" + listener.events.size()); + System.out.println("accessorFailureOrder...ok"); + } + + public interface SpecAccessor { + ContextSpec spec(); + } + + public interface IncompatibleAccessors { + InputStream wrap(InputStream input); + + String spec(); + + String tagLength(); + } + + public record TestSpec(String name) implements ContextSpec { + } + + public static class BaseDigest implements DigestContext { + private final AtomicInteger tagCalls = new AtomicInteger(); + + @Override + public InputStream wrap(InputStream upstream) { + return new java.io.SequenceInputStream(upstream, new ByteArrayInputStream(new byte[4])); + } + + @Override + public int tagLength() { + tagCalls.incrementAndGet(); + return 4; + } + + @Override + public void setVerificationApproach(VerificationBiPredicate strategy) { + // Not needed by this test context. + } + + @Override + public VerificationBiPredicate getVerificationCore() { + return null; + } + + @Override + public CryptoAlgorithm algorithm() { + return new AesAlgorithm(); + } + + @Override + public Key key() { + return NullKey.INSTANCE; + } + + @Override + public void close() { + // No resources. + } + } + + public static final class FirstSpecDigest extends BaseDigest implements SpecAccessor { + @Override + public ContextSpec spec() { + return new TestSpec("first"); + } + } + + public static final class SecondSpecDigest extends BaseDigest implements SpecAccessor { + @Override + public ContextSpec spec() { + return new TestSpec("second"); + } + } + + public static final class FailingDigest extends BaseDigest { + private final IllegalStateException failure; + + private FailingDigest(IllegalStateException failure) { + this.failure = failure; + } + + @Override + public int tagLength() { + throw failure; + } + } + + public static final class IncompatibleAccessorsContext implements EncryptionContext, IncompatibleAccessors { + @Override + public InputStream attach(InputStream upstream) { + return upstream; + } + + @Override + public InputStream wrap(InputStream input) { + return input; + } + + @Override + public String spec() { + return "not a context spec"; + } + + @Override + public String tagLength() { + return "not an integer"; + } + + @Override + public CryptoAlgorithm algorithm() { + return new AesAlgorithm(); + } + + @Override + public Key key() { + return NullKey.INSTANCE; + } + + @Override + public void close() { + // No resources. + } + } + + private static class MetadataListener implements AuditListener { + private String provider; + private Map specMeta; + + @Override + public void onContextCreatedMeta(String ctxId, String algoId, String providerName, KeyUsage role, + String keyFingerprint, Map metadata) { + provider = providerName; + specMeta = metadata; + } + } + + private static final class RecordingListener extends MetadataListener { + private final List events = new ArrayList<>(); + private Throwable failure; + + @Override + public void onContextCreatedMeta(String ctxId, String algoId, String providerName, KeyUsage role, + String keyFingerprint, Map metadata) { + super.onContextCreatedMeta(ctxId, algoId, providerName, role, keyFingerprint, metadata); + events.add("meta"); + } + + @Override + public void onFailure(String ctxId, String stage, String operation, Throwable cause) { + failure = cause; + events.add("failure"); + } + + @Override + public void onTagProduced(String ctxId, int tagLength, String policy) { + events.add("tag"); + } + } +} diff --git a/lib/src/test/java/zeroecho/core/audit/AuditedContextsRegressionTest.java b/lib/src/test/java/zeroecho/core/audit/AuditedContextsRegressionTest.java new file mode 100644 index 0000000..849d733 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/audit/AuditedContextsRegressionTest.java @@ -0,0 +1,261 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core.audit; + +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.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import javax.crypto.SecretKey; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.KeyUsage; +import zeroecho.core.context.AgreementContext; +import zeroecho.core.context.CryptoContext; +import zeroecho.core.context.DigestContext; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.context.MessageAgreementContext; +import zeroecho.core.spec.ContextSpec; + +class AuditedContextsRegressionTest { + + @Test + void unrelatedProxyGetsWrapped() throws Exception { + System.out.println("unrelatedProxyGetsWrapped"); + DigestContext target = mock(DigestContext.class); + when(target.tagLength()).thenReturn(2); + when(target.wrap(any(InputStream.class))).thenAnswer(invocation -> invocation.getArgument(0)); + DigestContext unrelated = (DigestContext) Proxy.newProxyInstance( + DigestContext.class.getClassLoader(), new Class[] { DigestContext.class }, + (proxy, method, arguments) -> { + try { + return method.invoke(target, arguments); + } catch (InvocationTargetException exception) { + throw exception.getCause(); + } + }); + + DigestContext wrapped = (DigestContext) AuditedContexts.wrap(unrelated, AuditListener.noop(), KeyUsage.DIGEST); + + assertNotSame(unrelated, wrapped); + assertTrue(Proxy.isProxyClass(wrapped.getClass())); + assertArrayEquals(new byte[] { 1, 2 }, wrapped.wrap(new ByteArrayInputStream(new byte[] { 1, 2 })).readAllBytes()); + assertSame(wrapped, AuditedContexts.wrap(wrapped, AuditListener.noop(), KeyUsage.DIGEST)); + System.out.println("...nestedProxy=true"); + System.out.println("unrelatedProxyGetsWrapped...ok"); + } + + @Test + void verifyEofKeepsBodyAccounting() throws Exception { + System.out.println("verifyEofKeepsBodyAccounting"); + RecordingListener listener = new RecordingListener(); + DigestContext target = mock(DigestContext.class); + when(target.tagLength()).thenReturn(8); + when(target.wrap(any(InputStream.class))).thenAnswer(invocation -> invocation.getArgument(0)); + DigestContext wrapped = (DigestContext) AuditedContexts.wrap(target, listener, KeyUsage.DIGEST); + wrapped.setExpectedTag(new byte[8]); + + assertArrayEquals(new byte[] { 1, 2, 3 }, + wrapped.wrap(new ByteArrayInputStream(new byte[] { 1, 2, 3 })).readAllBytes()); + + assertEquals(List.of(Boolean.TRUE), listener.verificationResults); + assertEquals(3, listener.bodyBytes); + assertEquals(0, listener.trailerBytes); + assertEquals(0, listener.tagEvents); + System.out.println("...body=3...trailer=0"); + System.out.println("verifyEofKeepsBodyAccounting...ok"); + } + + @Test + void verifyFailureSingleEvent() throws Exception { + System.out.println("verifyFailureSingleEvent"); + RecordingListener listener = new RecordingListener(); + IOException expected = new IOException("controlled verification failure"); + DigestContext target = mock(DigestContext.class); + when(target.tagLength()).thenReturn(4); + when(target.wrap(any(InputStream.class))).thenReturn(new InputStream() { + @Override + public int read() throws IOException { + throw expected; + } + }); + DigestContext wrapped = (DigestContext) AuditedContexts.wrap(target, listener, KeyUsage.DIGEST); + wrapped.setExpectedTag(new byte[4]); + + IOException actual = assertThrows(IOException.class, () -> { + try (InputStream input = wrapped.wrap(new ByteArrayInputStream(new byte[0]))) { + input.readAllBytes(); + } + }); + + assertSame(expected, actual); + assertEquals(List.of(expected), listener.failures); + assertEquals(List.of(Boolean.FALSE), listener.verificationResults); + System.out.println("...failures=1...verifyFalse=1"); + System.out.println("verifyFailureSingleEvent...ok"); + } + + @Test + void fingerprintsAvoidSecretBytes() { + System.out.println("fingerprintsAvoidSecretBytes"); + RecordingListener listener = new RecordingListener(); + SecretKey secretKey = mock(SecretKey.class); + when(secretKey.getAlgorithm()).thenReturn("CONTROLLED-SECRET"); + PrivateKey privateKey = mock(PrivateKey.class); + when(privateKey.getAlgorithm()).thenReturn("CONTROLLED-PRIVATE"); + + EncryptionContext secretContext = mock(EncryptionContext.class); + when(secretContext.key()).thenReturn(secretKey); + EncryptionContext privateContext = mock(EncryptionContext.class); + when(privateContext.key()).thenReturn(privateKey); + AuditedContexts.wrap(secretContext, listener, KeyUsage.ENCRYPT); + AuditedContexts.wrap(privateContext, listener, KeyUsage.DECRYPT); + + verify(secretKey, never()).getEncoded(); + verify(privateKey, never()).getEncoded(); + assertEquals(2, listener.fingerprints.size()); + assertTrue(listener.fingerprints.get(0).startsWith("CONTROLLED-SECRET:")); + assertTrue(listener.fingerprints.get(1).startsWith("CONTROLLED-PRIVATE:")); + assertFalse(listener.fingerprints.toString().contains("sensitive-key-bytes")); + + byte[] publicEncoding = { 1, 2, 3, 4 }; + PublicKey publicKey = mock(PublicKey.class); + when(publicKey.getAlgorithm()).thenReturn("CONTROLLED-PUBLIC"); + when(publicKey.getEncoded()).thenReturn(publicEncoding); + EncryptionContext publicContext = mock(EncryptionContext.class); + when(publicContext.key()).thenReturn(publicKey); + AuditedContexts.wrap(publicContext, listener, KeyUsage.ENCRYPT); + assertArrayEquals(new byte[4], publicEncoding); + assertFalse(listener.fingerprints.get(2).contains(Arrays.toString(new byte[] { 1, 2, 3, 4 }))); + + System.out.println("...privateEncodedCalls=0...secretEncodedCalls=0...publicEncodingCleared=true"); + System.out.println("fingerprintsAvoidSecretBytes...ok"); + } + + @Test + void proxyStringRepresentationDoesNotDelegate() { + System.out.println("proxyStringRepresentationDoesNotDelegate"); + SensitiveStringDigest target = new SensitiveStringDigest(); + + DigestContext wrapped = (DigestContext) AuditedContexts.wrap(target, AuditListener.noop(), KeyUsage.DIGEST); + String description = wrapped.toString(); + + assertTrue(description.startsWith("AuditedCryptoContext[")); + assertFalse(description.contains("SENSITIVE-TARGET-STATE")); + assertEquals(0, target.toStringCalls); + System.out.println("...description=" + description.substring(0, Math.min(30, description.length())) + "..."); + System.out.println("proxyStringRepresentationDoesNotDelegate...ok"); + } + + @Test + void agreementFailuresSingleEvent() { + System.out.println("agreementFailuresSingleEvent"); + RecordingListener listener = new RecordingListener(); + IllegalArgumentException peerFailure = new IllegalArgumentException("peer failure"); + IllegalStateException deriveFailure = new IllegalStateException("derive failure"); + AgreementContext agreement = mock(AgreementContext.class); + doThrow(peerFailure).when(agreement).setPeerPublic(any(PublicKey.class)); + when(agreement.deriveSecret()).thenThrow(deriveFailure); + AgreementContext wrappedAgreement = (AgreementContext) AuditedContexts.wrap( + agreement, listener, KeyUsage.AGREEMENT); + + assertSame(peerFailure, assertThrows(IllegalArgumentException.class, + () -> wrappedAgreement.setPeerPublic(mock(PublicKey.class)))); + assertSame(deriveFailure, assertThrows(IllegalStateException.class, wrappedAgreement::deriveSecret)); + + IllegalArgumentException setMessageFailure = new IllegalArgumentException("set message failure"); + IllegalStateException getMessageFailure = new IllegalStateException("get message failure"); + IllegalStateException messageDeriveFailure = new IllegalStateException("message derive failure"); + MessageAgreementContext messageAgreement = mock(MessageAgreementContext.class); + doThrow(setMessageFailure).when(messageAgreement).setPeerMessage(any(byte[].class)); + when(messageAgreement.getPeerMessage()).thenThrow(getMessageFailure); + when(messageAgreement.deriveSecret()).thenThrow(messageDeriveFailure); + MessageAgreementContext wrappedMessage = (MessageAgreementContext) AuditedContexts.wrap( + messageAgreement, listener, KeyUsage.AGREEMENT); + + assertSame(setMessageFailure, assertThrows(IllegalArgumentException.class, + () -> wrappedMessage.setPeerMessage(new byte[] { 1 }))); + assertSame(getMessageFailure, assertThrows(IllegalStateException.class, wrappedMessage::getPeerMessage)); + assertSame(messageDeriveFailure, + assertThrows(IllegalStateException.class, wrappedMessage::deriveSecret)); + assertEquals(List.of(peerFailure, deriveFailure, setMessageFailure, getMessageFailure, messageDeriveFailure), + listener.failures); + + System.out.println("...operations=5...failures=5"); + System.out.println("agreementFailuresSingleEvent...ok"); + } + + private static final class RecordingListener implements AuditListener { + private final List fingerprints = new ArrayList<>(); + private final List failures = new ArrayList<>(); + private final List verificationResults = new ArrayList<>(); + private long bodyBytes; + private long trailerBytes; + private int tagEvents; + + @Override + public void onContextCreatedMeta(String contextId, String algorithmId, String provider, KeyUsage role, + String keyFingerprint, Map specMeta) { + fingerprints.add(keyFingerprint); + } + + @Override + public void onProgress(String contextId, long bodyByteCount, long trailerByteCount) { + bodyBytes = bodyByteCount; + trailerBytes = trailerByteCount; + } + + @Override + public void onTagProduced(String contextId, int tagLength, String policy) { + tagEvents++; + } + + @Override + public void onVerifyResult(String contextId, boolean success, String policy, String expectedSource, + int tagLength) { + verificationResults.add(Boolean.valueOf(success)); + } + + @Override + public void onFailure(String contextId, String stage, String operation, Throwable cause) { + failures.add(cause); + } + + } + + private static final class SensitiveStringDigest extends AuditedContextsAccessorTest.BaseDigest { + private int toStringCalls; + + @Override + public String toString() { + toStringCalls++; + return "SENSITIVE-TARGET-STATE"; + } + } +} diff --git a/lib/src/test/java/zeroecho/core/audit/JulAuditListenerStdSecurityTest.java b/lib/src/test/java/zeroecho/core/audit/JulAuditListenerStdSecurityTest.java new file mode 100644 index 0000000..0b0f7ab --- /dev/null +++ b/lib/src/test/java/zeroecho/core/audit/JulAuditListenerStdSecurityTest.java @@ -0,0 +1,172 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core.audit; + +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 java.security.PrivateKey; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import javax.crypto.SecretKey; + +import org.junit.jupiter.api.Test; + +class JulAuditListenerStdSecurityTest { + private static final String SENSITIVE_TEXT = "controlled-sensitive-message"; + private static final AtomicInteger LOGGER_IDS = new AtomicInteger(); + + @Test + void keyLoggingUsesOnlySafeMetadataAndClearsPublicEncoding() { + System.out.println("keyLoggingUsesOnlySafeMetadataAndClearsPublicEncoding"); + RecordingHandler handler = new RecordingHandler(); + JulAuditListenerStd listener = listener(handler); + CountingSecretKey secretKey = new CountingSecretKey(); + CountingPrivateKey privateKey = new CountingPrivateKey(); + byte[] publicEncoding = { 1, 2, 3, 4 }; + PublicKey publicKey = new ControlledPublicKey(publicEncoding); + + listener.onKeyBuilt("secret", "provider", null, secretKey); + listener.onKeyBuilt("private", "provider", null, privateKey); + listener.onKeyBuilt("public", "provider", null, publicKey); + + assertEquals(0, secretKey.encodedCalls); + assertEquals(0, privateKey.encodedCalls); + assertArrayEquals(new byte[publicEncoding.length], publicEncoding); + assertFalse(handler.rendered().contains(Arrays.toString(new byte[] { 1, 2, 3, 4 }))); + System.out.println("...records=" + handler.records.size()); + System.out.println("keyLoggingUsesOnlySafeMetadataAndClearsPublicEncoding...ok"); + } + + @Test + void defaultFailureLoggingOmitsExceptionMessageAndThrowable() { + System.out.println("defaultFailureLoggingOmitsExceptionMessageAndThrowable"); + RecordingHandler handler = new RecordingHandler(); + JulAuditListenerStd listener = listener(handler); + + listener.onFailure("context", "read", "attach", new IllegalStateException(SENSITIVE_TEXT)); + + assertEquals(1, handler.records.size()); + assertFalse(handler.rendered().contains(SENSITIVE_TEXT)); + assertEquals(null, handler.records.get(0).getThrown()); + System.out.println("...failureRecords=1"); + System.out.println("defaultFailureLoggingOmitsExceptionMessageAndThrowable...ok"); + } + + private static JulAuditListenerStd listener(RecordingHandler handler) { + Logger logger = Logger.getLogger( + JulAuditListenerStdSecurityTest.class.getName() + "." + LOGGER_IDS.incrementAndGet()); + logger.setUseParentHandlers(false); + logger.setLevel(Level.ALL); + handler.setLevel(Level.ALL); + logger.addHandler(handler); + return JulAuditListenerStd.builder().logger(logger).build(); + } + + private static final class RecordingHandler extends Handler { + private final List records = new ArrayList<>(); + + @Override + public void publish(LogRecord record) { + records.add(record); + } + + @Override + public void flush() { + // No buffered state. + } + + @Override + public void close() { + records.clear(); + } + + private String rendered() { + StringBuilder result = new StringBuilder(); + for (LogRecord record : records) { + result.append(record.getMessage()); + if (record.getParameters() != null) { + result.append(Arrays.toString(record.getParameters())); + } + } + return result.toString(); + } + } + + private static final class CountingSecretKey implements SecretKey { + private static final long serialVersionUID = 1L; + private int encodedCalls; + + @Override + public String getAlgorithm() { + return "CONTROLLED-SECRET"; + } + + @Override + public String getFormat() { + return "RAW"; + } + + @Override + public byte[] getEncoded() { + encodedCalls++; + return SENSITIVE_TEXT.getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + } + + private static final class CountingPrivateKey implements PrivateKey { + private static final long serialVersionUID = 1L; + private int encodedCalls; + + @Override + public String getAlgorithm() { + return "CONTROLLED-PRIVATE"; + } + + @Override + public String getFormat() { + return "PKCS#8"; + } + + @Override + public byte[] getEncoded() { + encodedCalls++; + return SENSITIVE_TEXT.getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + } + + private static final class ControlledPublicKey implements PublicKey { + private static final long serialVersionUID = 1L; + private final byte[] encoding; + + private ControlledPublicKey(byte[] encoding) { + this.encoding = encoding; + } + + @Override + public String getAlgorithm() { + return "CONTROLLED-PUBLIC"; + } + + @Override + public String getFormat() { + return "X.509"; + } + + @Override + public byte[] getEncoded() { + return encoding; + } + } +} diff --git a/lib/src/test/java/zeroecho/core/io/CipherTransformInputStreamBuilderTest.java b/lib/src/test/java/zeroecho/core/io/CipherTransformInputStreamBuilderTest.java new file mode 100644 index 0000000..0e5fe66 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/io/CipherTransformInputStreamBuilderTest.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core.io; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.crypto.Cipher; + +import org.junit.jupiter.api.Test; + +class CipherTransformInputStreamBuilderTest { + @Test + void validatesEveryGeometryBoundaryWithoutAssertions() { + System.out.print("ChunkTransform/geometry..."); + assertThrows(IllegalArgumentException.class, () -> new TestStream(1, 1, 1, 0)); + assertThrows(IllegalArgumentException.class, () -> new TestStream(0, 1, 1, 0)); + assertThrows(IllegalArgumentException.class, () -> new TestStream(2, 0, 1, 0)); + assertThrows(IllegalArgumentException.class, () -> new TestStream(2, 1, 0, 0)); + assertThrows(IllegalArgumentException.class, () -> new TestStream(2, 1, 1, -1)); + assertThrows(IllegalArgumentException.class, () -> new TestStream(Integer.MAX_VALUE, 1, 2, 0)); + assertThrows(IllegalArgumentException.class, + () -> new TestStream(2, 1, Integer.MAX_VALUE, 1)); + assertThrows(IllegalArgumentException.class, + () -> new TestStream(2, Integer.MAX_VALUE, 1, 1)); + + new TestStream(2, 1, 1, 0); + new TestStream(2, 1, 1, 1); + System.out.println("ok"); + } + + @Test + void rejectsInvalidTransformOutputCounts() { + System.out.print("ChunkTransform/counts..."); + TestStream negative = new TestStream(2, 2, 1, 0); + negative.transformCount = -1; + assertThrows(IllegalStateException.class, negative::read); + + TestStream excessive = new TestStream(2, 2, 1, 0); + excessive.transformCount = 3; + assertThrows(IllegalStateException.class, excessive::read); + + TestStream finalExcessive = new TestStream(2, 2, 1, 0, new byte[0]); + finalExcessive.finalCount = 3; + assertThrows(IllegalStateException.class, finalExcessive::read); + System.out.println("ok"); + } + + @Test + void rejectsAesIndependentBlocksBeforeReadingUpstream() throws Exception { + System.out.print("CipherBuilder/algorithm-guard..."); + CountingInputStream gcmInput = new CountingInputStream(); + Cipher gcm = Cipher.getInstance("AES/GCM/NoPadding"); + assertThrows(IllegalArgumentException.class, + () -> CipherTransformInputStreamBuilder.builder().withUpstream(gcmInput).withCipher(gcm) + .withIndependentBlocks().build()); + assertEquals(0, gcmInput.reads); + + CountingInputStream cbcInput = new CountingInputStream(); + Cipher cbc = Cipher.getInstance("AES/CBC/PKCS5Padding"); + assertThrows(IllegalArgumentException.class, + () -> CipherTransformInputStreamBuilder.builder().withUpstream(cbcInput).withCipher(cbc) + .withLeftZeroPadding(true).withIndependentBlocks().build()); + assertEquals(0, cbcInput.reads); + System.out.println("ok"); + } + + private static final class TestStream extends AbstractChunkTransformInputStream { + private int transformCount; + private int finalCount; + + private TestStream(int inputChunk, int outputChunk, int chunks, int finalChunks) { + this(inputChunk, outputChunk, chunks, finalChunks, new byte[] { 1, 2 }); + } + + private TestStream(int inputChunk, int outputChunk, int chunks, int finalChunks, byte[] input) { + super(new ByteArrayInputStream(input), inputChunk, outputChunk, chunks, finalChunks); + transformCount = outputChunk; + } + + @Override + protected int transform(byte[] input, int inputOffset, int inputChunks, byte[] output) { + return transformCount; + } + + @Override + protected int doFinal(byte[] input, int inputOffset, int length, byte[] output, int outputOffset) { + return finalCount; + } + } + + private static final class CountingInputStream extends InputStream { + private int reads; + + @Override + public int read() throws IOException { + reads++; + return -1; + } + } +} diff --git a/lib/src/test/java/zeroecho/core/marshal/PairSeqCodecTest.java b/lib/src/test/java/zeroecho/core/marshal/PairSeqCodecTest.java new file mode 100644 index 0000000..2445e08 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/marshal/PairSeqCodecTest.java @@ -0,0 +1,383 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core.marshal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.junit.jupiter.api.Test; + +/** + * Verifies cached accessor plans for {@link PairSeqCodec}. + */ +class PairSeqCodecTest { + + @Test + void marshalAndStaticFactoryUnmarshalSucceed() { + System.out.println("marshalAndStaticFactoryUnmarshalSucceed"); + PairSeqCodec codec = new PairSeqCodec<>(FactoryValue.class); + + PairSeq representation = codec.marshal(new FactoryValue("alpha")); + FactoryValue decoded = codec.unmarshal(representation); + + assertEquals("alpha", representation.valAt(0)); + assertEquals("alpha", decoded.value); + System.out.println("...decoded=" + decoded.value); + System.out.println("marshalAndStaticFactoryUnmarshalSucceed...ok"); + } + + @Test + void repeatedOperationsReuseCachedPlans() { + System.out.println("repeatedOperationsReuseCachedPlans"); + PairSeqCodec codec = new PairSeqCodec<>(FactoryValue.class); + Object marshalPlan = PairSeqCodec.cachedMarshalPlan(FactoryValue.class); + Object unmarshalPlan = PairSeqCodec.cachedUnmarshalPlan(FactoryValue.class); + + for (int index = 0; index < 20; index++) { + PairSeq representation = codec.marshal(new FactoryValue(Integer.toString(index))); + assertEquals(Integer.toString(index), codec.unmarshal(representation).value); + } + assertSame(marshalPlan, PairSeqCodec.cachedMarshalPlan(FactoryValue.class)); + assertSame(unmarshalPlan, PairSeqCodec.cachedUnmarshalPlan(FactoryValue.class)); + assertEquals(1, PairSeqCodec.marshalResolutionCount(FactoryValue.class)); + assertEquals(1, PairSeqCodec.unmarshalResolutionCount(FactoryValue.class)); + + System.out.println("...iterations=20"); + System.out.println("repeatedOperationsReuseCachedPlans...ok"); + } + + @Test + void separateRuntimeClassesUseSeparateMarshalPlans() { + System.out.println("separateRuntimeClassesUseSeparateMarshalPlans"); + Object first = PairSeqCodec.cachedMarshalPlan(FactoryValue.class); + Object second = PairSeqCodec.cachedMarshalPlan(ConstructorValue.class); + Object firstUnmarshal = PairSeqCodec.cachedUnmarshalPlan(FactoryValue.class); + Object secondUnmarshal = PairSeqCodec.cachedUnmarshalPlan(ConstructorValue.class); + + assertNotSame(first, second); + assertNotSame(firstUnmarshal, secondUnmarshal); + + System.out.println("...separatePlans=true"); + System.out.println("separateRuntimeClassesUseSeparateMarshalPlans...ok"); + } + + @Test + void constructorFallbackUnmarshalsSuccessfully() { + System.out.println("constructorFallbackUnmarshalsSuccessfully"); + PairSeqCodec codec = new PairSeqCodec<>(ConstructorValue.class); + + ConstructorValue decoded = codec.unmarshal(PairSeq.of("value", "beta")); + + assertEquals("beta", decoded.value); + System.out.println("...decoded=" + decoded.value); + System.out.println("constructorFallbackUnmarshalsSuccessfully...ok"); + } + + @Test + void broadlyDeclaredFactoryReturnRemainsCompatible() { + System.out.println("broadlyDeclaredFactoryReturnRemainsCompatible"); + PairSeqCodec codec = new PairSeqCodec<>(BroadFactoryValue.class); + + BroadFactoryValue decoded = codec.unmarshal(PairSeq.of("value", "broad")); + + assertEquals("broad", decoded.value); + System.out.println("...decoded=" + decoded.value); + System.out.println("broadlyDeclaredFactoryReturnRemainsCompatible...ok"); + } + + @Test + void missingAndIncompatibleMethodsReportContext() { + System.out.println("missingAndIncompatibleMethodsReportContext"); + PairSeqCodec missingCodec = new PairSeqCodec<>(MissingValue.class); + PairSeqCodec wrongMarshalCodec = new PairSeqCodec<>(WrongMarshalValue.class); + PairSeqCodec wrongFactoryCodec = new PairSeqCodec<>(WrongFactoryValue.class); + + IllegalStateException missingMarshal = assertThrows(IllegalStateException.class, + () -> missingCodec.marshal(new MissingValue())); + IllegalStateException missingUnmarshal = assertThrows(IllegalStateException.class, + () -> missingCodec.unmarshal(PairSeq.of())); + IllegalStateException wrongMarshal = assertThrows(IllegalStateException.class, + () -> wrongMarshalCodec.marshal(new WrongMarshalValue())); + IllegalStateException wrongFactory = assertThrows(IllegalStateException.class, + () -> wrongFactoryCodec.unmarshal(PairSeq.of())); + + assertTrue(missingMarshal.getMessage().contains("marshal")); + assertTrue(missingUnmarshal.getMessage().contains("unmarshal")); + assertTrue(wrongMarshal.getMessage().contains("must return PairSeq")); + assertTrue(wrongFactory.getMessage().contains("must return")); + System.out.println("...negativeCases=4"); + System.out.println("missingAndIncompatibleMethodsReportContext...ok"); + } + + @Test + void targetFailuresRetainInvocationContext() { + System.out.println("targetFailuresRetainInvocationContext"); + PairSeqCodec codec = new PairSeqCodec<>(ThrowingValue.class); + + IllegalStateException marshalFailure = assertThrows(IllegalStateException.class, + () -> codec.marshal(new ThrowingValue())); + IllegalStateException unmarshalFailure = assertThrows(IllegalStateException.class, + () -> codec.unmarshal(PairSeq.of())); + + assertTrue(marshalFailure.getMessage().contains("marshal() failed")); + assertTrue(unmarshalFailure.getMessage().contains("unmarshal(PairSeq) failed")); + assertTrue(marshalFailure.getCause() instanceof java.lang.reflect.InvocationTargetException); + assertTrue(unmarshalFailure.getCause() instanceof java.lang.reflect.InvocationTargetException); + System.out.println("...causesPreserved=true"); + System.out.println("targetFailuresRetainInvocationContext...ok"); + } + + @Test + void nullInputsAreRejected() { + System.out.println("nullInputsAreRejected"); + PairSeqCodec codec = new PairSeqCodec<>(FactoryValue.class); + + assertThrows(NullPointerException.class, () -> new PairSeqCodec(null)); + assertThrows(NullPointerException.class, () -> codec.marshal(null)); + assertThrows(NullPointerException.class, () -> codec.unmarshal(null)); + + System.out.println("...nullCases=3"); + System.out.println("nullInputsAreRejected...ok"); + } + + @Test + void concurrentFirstAccessUsesOneCachedPlan() throws Exception { + System.out.println("concurrentFirstAccessUsesOneCachedPlan"); + int taskCount = 24; + CountDownLatch start = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(8); + try { + List> futures = new ArrayList<>(); + for (int index = 0; index < taskCount; index++) { + futures.add(executor.submit(() -> { + start.await(); + PairSeqCodec codec = new PairSeqCodec<>(ConcurrentValue.class); + ConcurrentValue value = codec.unmarshal(codec.marshal(new ConcurrentValue("gamma"))); + assertEquals("gamma", value.value); + return new PlanPair(PairSeqCodec.cachedMarshalPlan(ConcurrentValue.class), + PairSeqCodec.cachedUnmarshalPlan(ConcurrentValue.class)); + })); + } + start.countDown(); + PlanPair expectedPlans = futures.get(0).get(); + for (Future future : futures) { + PlanPair actualPlans = future.get(); + assertSame(expectedPlans.marshal(), actualPlans.marshal()); + assertSame(expectedPlans.unmarshal(), actualPlans.unmarshal()); + } + assertEquals(1, PairSeqCodec.marshalResolutionCount(ConcurrentValue.class)); + assertEquals(1, PairSeqCodec.unmarshalResolutionCount(ConcurrentValue.class)); + } finally { + executor.shutdownNow(); + } + + System.out.println("...completedTasks=" + taskCount); + System.out.println("concurrentFirstAccessUsesOneCachedPlan...ok"); + } + + /** + * Test value exposing marshal and static-factory conventions. + */ + public static final class FactoryValue { + private final String value; + + /** + * Creates a test value. + * + * @param value stored value + */ + public FactoryValue(String value) { + this.value = value; + } + + /** + * Marshals this test value. + * + * @return pair representation + */ + public PairSeq marshal() { + return PairSeq.of("value", value); + } + + /** + * Reconstructs a test value. + * + * @param representation pair representation + * @return reconstructed value + */ + public static FactoryValue unmarshal(PairSeq representation) { + return new FactoryValue(representation.valAt(0)); + } + } + + /** + * Test value exposing marshal and constructor conventions. + */ + public static final class ConstructorValue { + private final String value; + + /** + * Creates a test value. + * + * @param value stored value + */ + public ConstructorValue(String value) { + this.value = value; + } + + /** + * Reconstructs a test value. + * + * @param representation pair representation + */ + public ConstructorValue(PairSeq representation) { + this(representation.valAt(0)); + } + + /** + * Marshals this test value. + * + * @return pair representation + */ + public PairSeq marshal() { + return PairSeq.of("value", value); + } + } + + /** + * Test value with no codec conventions. + */ + public static final class MissingValue { + } + + /** + * Test value with an incompatible marshal return. + */ + public static final class WrongMarshalValue { + /** + * Returns an intentionally incompatible representation. + * + * @return incompatible value + */ + public String marshal() { + return "wrong"; + } + } + + /** + * Test value with an incompatible factory return. + */ + public static final class WrongFactoryValue { + /** + * Returns an intentionally incompatible value. + * + * @param representation ignored representation + * @return incompatible value + */ + public static String unmarshal(PairSeq representation) { + return representation.valAt(0); + } + } + + /** + * Test value whose codec operations fail. + */ + public static final class ThrowingValue { + /** + * Fails during marshalling. + * + * @return no value + * @throws IllegalArgumentException always + */ + public PairSeq marshal() { + throw new IllegalArgumentException("marshal target"); + } + + /** + * Fails during unmarshalling. + * + * @param representation ignored representation + * @return no value + * @throws IllegalArgumentException always + */ + public static ThrowingValue unmarshal(PairSeq representation) { + throw new IllegalArgumentException("unmarshal target"); + } + } + + /** + * Test value whose factory declares a compatible broad return type. + */ + public static final class BroadFactoryValue { + private final String value; + + /** + * Creates a test value. + * + * @param value stored value + */ + public BroadFactoryValue(String value) { + this.value = value; + } + + /** + * Reconstructs a test value through a broadly declared return type. + * + * @param representation pair representation + * @return reconstructed value as {@link Object} + */ + public static Object unmarshal(PairSeq representation) { + return new BroadFactoryValue(representation.valAt(0)); + } + } + + /** + * Test value reserved for concurrent cold-cache access. + */ + public static final class ConcurrentValue { + private final String value; + + /** + * Creates a test value. + * + * @param value stored value + */ + public ConcurrentValue(String value) { + this.value = value; + } + + /** + * Marshals this test value. + * + * @return pair representation + */ + public PairSeq marshal() { + return PairSeq.of("value", value); + } + + /** + * Reconstructs a test value. + * + * @param representation pair representation + * @return reconstructed value + */ + public static ConcurrentValue unmarshal(PairSeq representation) { + return new ConcurrentValue(representation.valAt(0)); + } + } + + private record PlanPair(Object marshal, Object unmarshal) { + } +} diff --git a/lib/src/test/java/zeroecho/core/marshal/PairSeqTest.java b/lib/src/test/java/zeroecho/core/marshal/PairSeqTest.java new file mode 100644 index 0000000..8604fd4 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/marshal/PairSeqTest.java @@ -0,0 +1,138 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core.marshal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.io.StringReader; +import java.io.UncheckedIOException; + +import org.junit.jupiter.api.Test; + +class PairSeqTest { + @Test + void checkedRoundTrip() throws Exception { + System.out.println("checkedRoundTrip"); + PairSeq empty = PairSeq.of(); + assertEquals(0, empty.size()); + + PairSeq original = PairSeq.of("a", "1", "b", ""); + StringBuilder output = new StringBuilder(); + original.writeTo(output); + PairSeq decoded = PairSeq.readFrom(new StringReader(output.toString())); + + assertEquals(2, decoded.size()); + assertEquals("a", decoded.keyAt(0)); + assertEquals("1", decoded.valAt(0)); + assertEquals("", decoded.valAt(1)); + System.out.println("...pairs=" + decoded.size()); + System.out.println("checkedRoundTrip...ok"); + } + + @Test + void validationAndOwnership() { + System.out.println("validationAndOwnership"); + assertThrows(IllegalArgumentException.class, () -> PairSeq.of((String[]) null)); + assertThrows(IllegalArgumentException.class, () -> PairSeq.of("key")); + assertEquals("pair 0 key must not be null", + assertThrows(IllegalArgumentException.class, () -> PairSeq.of(null, "value")).getMessage()); + assertEquals("pair 0 value must not be null", + assertThrows(IllegalArgumentException.class, () -> PairSeq.of("key", null)).getMessage()); + assertEquals("pair 1 key must not be null", + assertThrows(IllegalArgumentException.class, + () -> PairSeq.of("first", "value", null, "other")).getMessage()); + + String[] source = { "key", "value" }; + PairSeq sequence = PairSeq.of(source); + source[0] = "changed"; + assertEquals("key", sequence.keyAt(0)); + assertNotSame(source, sequence); + System.out.println("...owned=true"); + System.out.println("validationAndOwnership...ok"); + } + + @Test + void failurePropagation() { + System.out.println("failurePropagation"); + IOException immediate = new IOException("immediate"); + IOException checked = assertThrows(IOException.class, + () -> PairSeq.of("a", "b").writeTo(new FailingAppendable(0, immediate))); + assertSame(immediate, checked); + + IOException partial = new IOException("partial"); + FailingAppendable partialOutput = new FailingAppendable(2, partial); + IOException partialActual = assertThrows(IOException.class, + () -> PairSeq.of("a", "b").writeTo(partialOutput)); + assertSame(partial, partialActual); + assertEquals("a=", partialOutput.output.toString()); + + IllegalStateException runtime = new IllegalStateException("runtime"); + IllegalStateException runtimeActual = assertThrows(IllegalStateException.class, + () -> PairSeq.of("a", "b").writeTo(new RuntimeFailingAppendable(runtime))); + assertSame(runtime, runtimeActual); + System.out.println("...partialChars=" + partialOutput.output.length()); + System.out.println("failurePropagation...ok"); + } + + private static final class FailingAppendable implements Appendable { + private final int acceptedCharacters; + private final IOException failure; + private final StringBuilder output = new StringBuilder(); + + private FailingAppendable(int acceptedCharacters, IOException failure) { + this.acceptedCharacters = acceptedCharacters; + this.failure = failure; + } + + @Override + public Appendable append(CharSequence sequence) throws IOException { + for (int index = 0; index < sequence.length(); index++) { + append(sequence.charAt(index)); + } + return this; + } + + @Override + public Appendable append(CharSequence sequence, int start, int end) throws IOException { + return append(sequence.subSequence(start, end)); + } + + @Override + public Appendable append(char character) throws IOException { + if (output.length() == acceptedCharacters) { + throw failure; + } + output.append(character); + return this; + } + } + + private static final class RuntimeFailingAppendable implements Appendable { + private final IllegalStateException failure; + + private RuntimeFailingAppendable(IllegalStateException failure) { + this.failure = failure; + } + + @Override + public Appendable append(CharSequence sequence) { + throw failure; + } + + @Override + public Appendable append(CharSequence sequence, int start, int end) { + throw failure; + } + + @Override + public Appendable append(char character) { + throw failure; + } + } +} diff --git a/lib/src/test/java/zeroecho/core/storage/KeyringStoreDynamicTest.java b/lib/src/test/java/zeroecho/core/storage/KeyringStoreDynamicTest.java index 3519b9e..afcbc30 100644 --- a/lib/src/test/java/zeroecho/core/storage/KeyringStoreDynamicTest.java +++ b/lib/src/test/java/zeroecho/core/storage/KeyringStoreDynamicTest.java @@ -62,8 +62,10 @@ import zeroecho.core.alg.rsa.RsaKeyGenSpec; import zeroecho.core.alg.rsa.RsaPrivateKeySpec; import zeroecho.core.alg.rsa.RsaPublicKeySpec; import zeroecho.core.spec.AlgorithmKeySpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; -import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.core.KeyOperation; +import zeroecho.core.KeyOperationInfo; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.SymmetricKeyGenerator; import zeroecho.sdk.util.BouncyCastleActivator; public class KeyringStoreDynamicTest { @@ -183,19 +185,19 @@ public class KeyringStoreDynamicTest { logBegin(); Path keyringPath = tempDir.resolve("keyring-" + System.nanoTime() + ".txt"); - KeyringStore store = new KeyringStore(); + KeyringStore store = new KeyringStore(new zeroecho.sdk.ZeroEchoSession()); - CryptoAlgorithm algo = CryptoAlgorithms.require("RSA"); - KeyPair kp = algo.generateKeyPair(RsaKeyGenSpec.rsa4096()); + zeroecho.sdk.ZeroEchoSession session = new zeroecho.sdk.ZeroEchoSession(); + KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa4096()); store.putPrivate("alice.priv", "RSA", new RsaPrivateKeySpec(kp.getPrivate().getEncoded())); store.putPublic("alice.pub", "RSA", new RsaPublicKeySpec(kp.getPublic().getEncoded())); - kp = algo.generateKeyPair(RsaKeyGenSpec.rsa4096()); + kp = session.keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa4096()); store.putPrivate("bob.priv", "RSA", new RsaPrivateKeySpec(kp.getPrivate().getEncoded())); store.putPublic("bob.pub", "RSA", new RsaPublicKeySpec(kp.getPublic().getEncoded())); store.save(keyringPath); - store = KeyringStore.load(keyringPath); + store = KeyringStore.load(new zeroecho.sdk.ZeroEchoSession(), keyringPath); String s = store.exportText(Collections.singleton("alice.pub")); assertTrue(s.contains("# KeyringStore v1\n")); @@ -214,7 +216,7 @@ public class KeyringStoreDynamicTest { void keyring_dynamic_population_roundtrip_and_dump(@TempDir Path tempDir) throws Exception { logBegin(); - KeyringStore store = new KeyringStore(); + KeyringStore store = new KeyringStore(new zeroecho.sdk.ZeroEchoSession()); Set ids = CryptoAlgorithms.available(); System.out.println("...algorithms discovered: " + ids); @@ -225,34 +227,35 @@ public class KeyringStoreDynamicTest { CryptoAlgorithm alg = CryptoAlgorithms.require(id); System.out.println("\n-- " + id + " --"); - if (!alg.asymmetricBuildersInfo().isEmpty()) { + if (alg.keyOperations().stream() + .anyMatch(info -> info.operation() == KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE)) { int perAlg = 0; - for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) { - if (bi.defaultKeySpec == null) { + for (KeyOperationInfo bi : alg.keyOperations()) { + if (bi.operation() != KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE || bi.defaultSpec() == null) { continue; } try { @SuppressWarnings("unchecked") - Class genSpecType = (Class) bi.specType; - AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(genSpecType); + Class genSpecType = (Class) bi.specType(); + AsymmetricKeyPairGenerator b = alg.asymmetricKeyPairGenerator(genSpecType); - AlgorithmKeySpec genSpec = (AlgorithmKeySpec) bi.defaultKeySpec; + AlgorithmKeySpec genSpec = bi.defaultSpec(); KeyPair kp = b.generateKeyPair(genSpec); PublicKey pub = kp.getPublic(); PrivateKey prv = kp.getPrivate(); Class pubImpType = null; Class prvImpType = null; - for (CryptoAlgorithm.AsymBuilderInfo x : alg.asymmetricBuildersInfo()) { - if (looksLikeImportSpecForPublic(x.specType)) { - pubImpType = x.specType; - } else if (looksLikeImportSpecForPrivate(x.specType)) { - prvImpType = x.specType; + for (KeyOperationInfo x : alg.keyOperations()) { + if (x.operation() == KeyOperation.ASYMMETRIC_PUBLIC_IMPORT) { + pubImpType = x.specType(); + } else if (x.operation() == KeyOperation.ASYMMETRIC_PRIVATE_IMPORT) { + prvImpType = x.specType(); } } if (pubImpType != null) { AlgorithmKeySpec pubSpec = makeImportSpec(pubImpType, pub.getEncoded(), id, - bi.defaultKeySpec); + bi.defaultSpec()); String alias = id.toLowerCase() + "-pub-" + perAlg; store.putPublic(alias, id, pubSpec); System.out.println("..." + alias + " saved, len=" + encLen(pub.getEncoded())); @@ -262,7 +265,7 @@ public class KeyringStoreDynamicTest { } if (prvImpType != null) { AlgorithmKeySpec prvSpec = makeImportSpec(prvImpType, prv.getEncoded(), id, - bi.defaultKeySpec); + bi.defaultSpec()); String alias = id.toLowerCase() + "-prv-" + perAlg; store.putPrivate(alias, id, prvSpec); System.out.println("..." + alias + " saved, len=" + encLen(prv.getEncoded())); @@ -282,46 +285,37 @@ public class KeyringStoreDynamicTest { } } - if (!alg.symmetricBuildersInfo().isEmpty()) { + if (alg.keyOperations().stream() + .anyMatch(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE)) { int perAlg = 0; - for (CryptoAlgorithm.SymBuilderInfo bi : alg.symmetricBuildersInfo()) { - if (bi.defaultKeySpec() == null) { + for (KeyOperationInfo bi : alg.keyOperations()) { + if (bi.operation() != KeyOperation.SYMMETRIC_GENERATE || bi.defaultSpec() == null) { continue; } try { @SuppressWarnings("unchecked") Class genSpecType = (Class) bi.specType(); - SymmetricKeyBuilder b = alg.symmetricKeyBuilder(genSpecType); + SymmetricKeyGenerator b = alg.symmetricKeyGenerator(genSpecType); - AlgorithmKeySpec genSpec = (AlgorithmKeySpec) bi.defaultKeySpec(); + AlgorithmKeySpec genSpec = bi.defaultSpec(); SecretKey sk = b.generateSecret(genSpec); Class impType = null; - for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) { - if (looksLikeImportSpecForSecret(x.specType())) { + for (KeyOperationInfo x : alg.keyOperations()) { + if (x.operation() == KeyOperation.SYMMETRIC_IMPORT + && looksLikeImportSpecForSecret(x.specType())) { impType = x.specType(); } } - if (impType == null) { - for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) { - try { - x.specType().getConstructor(byte[].class); - impType = x.specType(); - break; - } catch (NoSuchMethodException ignored) { - } - } - } if (impType != null) { byte[] raw = sk.getEncoded(); if (raw == null) { raw = randomBytes(32); } - AlgorithmKeySpec imp = makeImportSpec(impType, raw, id, bi.defaultKeySpec()); + AlgorithmKeySpec imp = makeImportSpec(impType, raw, id, bi.defaultSpec()); String alias = id.toLowerCase() + "-sec-" + perAlg; store.putSecret(alias, id, imp); - System.out.println("..." + alias + " saved, len=" + (raw == null ? 0 : raw.length) + " " - + Base64.getEncoder().withoutPadding().encodeToString(raw)); + System.out.println("..." + alias + " saved, len=" + raw.length); totalAdded++; } else { System.out.println("...*** SKIP *** no symmetric import spec for " + id); @@ -344,7 +338,7 @@ public class KeyringStoreDynamicTest { System.out.println("\n...saved keyring: " + keyringPath.getFileName()); System.out.println("...entries stored: " + totalAdded); - KeyringStore loaded = KeyringStore.load(keyringPath); + KeyringStore loaded = KeyringStore.load(new zeroecho.sdk.ZeroEchoSession(), keyringPath); assertTrue(loaded.aliases().size() >= Math.min(totalAdded, 1), "no entries reloaded"); int ok = 0; diff --git a/lib/src/test/java/zeroecho/core/storage/KeyringStoreSecurityTest.java b/lib/src/test/java/zeroecho/core/storage/KeyringStoreSecurityTest.java new file mode 100644 index 0000000..1144cc5 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/storage/KeyringStoreSecurityTest.java @@ -0,0 +1,138 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.core.storage; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.security.GeneralSecurityException; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.security.auth.DestroyFailedException; +import javax.security.auth.Destroyable; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.sdk.ZeroEchoSession; + +class KeyringStoreSecurityTest { + private static final AtomicBoolean UNREGISTERED_INITIALIZED = new AtomicBoolean(); + + @Test + void rejectsUnregisteredPersistedSpecBeforeClassInitialization() throws Exception { + System.out.println("rejectsUnregisteredPersistedSpecBeforeClassInitialization"); + KeyringStore store = new KeyringStore(new ZeroEchoSession()); + String text = "# KeyringStore v1\n" + + "@entry\n" + + "alias=attacker.pub\n" + + "algorithm=RSA\n" + + "kind=PUBLIC_KEY\n" + + "spec=zeroecho.core.storage.KeyringStoreSecurityTest$UnregisteredSpec\n\n"; + store.importText(text, false); + + IllegalArgumentException failure = assertThrows(IllegalArgumentException.class, + () -> store.getPublic("attacker")); + + assertFalse(UNREGISTERED_INITIALIZED.get()); + assertTrue(failure.getMessage().contains("not registered")); + System.out.println("...classInitialized=false"); + System.out.println("rejectsUnregisteredPersistedSpecBeforeClassInitialization...ok"); + } + + @Test + void rejectsSpecRegisteredForDifferentOperation() throws Exception { + System.out.println("rejectsSpecRegisteredForDifferentOperation"); + KeyringStore store = new KeyringStore(new ZeroEchoSession()); + String text = "# KeyringStore v1\n" + + "@entry\n" + + "alias=mismatch.pub\n" + + "algorithm=RSA\n" + + "kind=PUBLIC_KEY\n" + + "spec=zeroecho.core.alg.rsa.RsaPrivateKeySpec\n\n"; + store.importText(text, false); + + assertThrows(IllegalArgumentException.class, () -> store.getPublic("mismatch")); + System.out.println("...mismatchedOperationRejected=true"); + System.out.println("rejectsSpecRegisteredForDifferentOperation...ok"); + } + + @Test + void temporarySpecDestructionIsIdempotentAndObservable() throws Exception { + System.out.println("temporarySpecDestructionIsIdempotentAndObservable"); + ControlledDestroyableSpec spec = new ControlledDestroyableSpec(false); + + KeyringStore.destroyTemporarySpec(spec, null); + KeyringStore.destroyTemporarySpec(spec, null); + + assertTrue(spec.isDestroyed()); + assertEquals(1, spec.destroyCalls); + System.out.println("...destroyCalls=" + spec.destroyCalls); + System.out.println("temporarySpecDestructionIsIdempotentAndObservable...ok"); + } + + @Test + void destructionFailureIsSuppressedOnPrimaryFailure() throws Exception { + System.out.println("destructionFailureIsSuppressedOnPrimaryFailure"); + ControlledDestroyableSpec spec = new ControlledDestroyableSpec(true); + IllegalStateException primary = new IllegalStateException("controlled primary"); + + KeyringStore.destroyTemporarySpec(spec, primary); + + assertEquals(1, primary.getSuppressed().length); + assertTrue(primary.getSuppressed()[0] instanceof DestroyFailedException); + System.out.println("...suppressedFailures=1"); + System.out.println("destructionFailureIsSuppressedOnPrimaryFailure...ok"); + } + + @Test + void destructionFailureWithoutPrimaryUsesSecurityExceptionFamily() { + System.out.println("destructionFailureWithoutPrimaryUsesSecurityExceptionFamily"); + ControlledDestroyableSpec spec = new ControlledDestroyableSpec(true); + + GeneralSecurityException failure = assertThrows(GeneralSecurityException.class, + () -> KeyringStore.destroyTemporarySpec(spec, null)); + + assertSame(DestroyFailedException.class, failure.getCause().getClass()); + System.out.println("...failureType=" + failure.getClass().getSimpleName()); + System.out.println("destructionFailureWithoutPrimaryUsesSecurityExceptionFamily...ok"); + } + + /** + * A deliberately unregistered type whose initialization must never occur. + */ + public static final class UnregisteredSpec implements AlgorithmKeySpec { + static { + UNREGISTERED_INITIALIZED.set(true); + } + } + + private static final class ControlledDestroyableSpec implements AlgorithmKeySpec, Destroyable { + private final boolean fail; + private boolean destroyed; + private int destroyCalls; + + private ControlledDestroyableSpec(boolean fail) { + this.fail = fail; + } + + @Override + public void destroy() throws DestroyFailedException { + destroyCalls++; + if (fail) { + throw new DestroyFailedException("controlled destruction failure"); + } + destroyed = true; + } + + @Override + public boolean isDestroyed() { + return destroyed; + } + } +} diff --git a/lib/src/test/java/zeroecho/sdk/ZeroEchoSessionDestroyKeyTest.java b/lib/src/test/java/zeroecho/sdk/ZeroEchoSessionDestroyKeyTest.java new file mode 100644 index 0000000..2b01687 --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/ZeroEchoSessionDestroyKeyTest.java @@ -0,0 +1,202 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.sdk; + +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.security.Key; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import javax.security.auth.DestroyFailedException; +import javax.security.auth.Destroyable; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.audit.AuditListener; + +class ZeroEchoSessionDestroyKeyTest { + @Test + void strictDestroyDistinguishesEveryLifecycleOutcome() throws Exception { + System.out.print("ZeroEchoSession/destroy-strict..."); + AtomicInteger audits = new AtomicInteger(); + ZeroEchoSession session = new ZeroEchoSession().withAuditListener(new AuditListener() { + @Override + public void onKeyDestroyed(String id, String provider, Key key) { + audits.incrementAndGet(); + } + }); + + TestKey success = new TestKey(Behavior.SUCCESS); + assertTrue(session.destroyKey("test", "provider", success)); + assertFalse(session.destroyKey("test", "provider", success)); + assertEquals(1, audits.get()); + assertFalse(session.destroyKey("test", "provider", new PlainKey())); + assertThrows(NullPointerException.class, () -> session.destroyKey("test", "provider", null)); + assertThrows(DestroyFailedException.class, + () -> session.destroyKey("test", "provider", new TestKey(Behavior.FAIL_CHECKED))); + assertThrows(DestroyFailedException.class, + () -> session.destroyKey("test", "provider", new TestKey(Behavior.NO_TRANSITION))); + assertThrows(IllegalStateException.class, + () -> session.destroyKey("test", "provider", new TestKey(Behavior.FAIL_RUNTIME))); + assertEquals(1, audits.get()); + System.out.println("ok"); + } + + @Test + void concurrentStrictDestroyReportsAndAuditsOneTransition() throws Exception { + System.out.print("ZeroEchoSession/destroy-concurrent..."); + AtomicInteger audits = new AtomicInteger(); + ZeroEchoSession session = new ZeroEchoSession().withAuditListener(new AuditListener() { + @Override + public void onKeyDestroyed(String id, String provider, Key key) { + audits.incrementAndGet(); + } + }); + BlockingKey key = new BlockingKey(); + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + Future first = executor.submit(() -> session.destroyKey("test", "provider", key)); + key.destroyEntered.await(); + Future second = executor.submit(() -> session.destroyKey("test", "provider", key)); + key.allowDestroy.countDown(); + + boolean firstResult = first.get(); + boolean secondResult = second.get(); + assertTrue(firstResult ^ secondResult); + assertEquals(1, audits.get()); + assertEquals(1, key.destroyCalls.get()); + } finally { + executor.shutdownNow(); + } + System.out.println("ok"); + } + + private enum Behavior { + SUCCESS, + FAIL_CHECKED, + FAIL_RUNTIME, + NO_TRANSITION + } + + private static final class TestKey implements Key, Destroyable { + private static final long serialVersionUID = 1L; + private static final String SECRET = "secret-key-marker"; + private final Behavior behavior; + private boolean destroyed; + private boolean encodedCalled; + private boolean toStringCalled; + + private TestKey(Behavior behavior) { + this.behavior = behavior; + } + + @Override + public void destroy() throws DestroyFailedException { + switch (behavior) { + case SUCCESS -> destroyed = true; + case FAIL_CHECKED -> throw new DestroyFailedException(SECRET); + case FAIL_RUNTIME -> throw new IllegalStateException(SECRET); + case NO_TRANSITION -> { + // Intentionally does not transition. + } + } + } + + @Override + public boolean isDestroyed() { + return destroyed; + } + + @Override + public String getAlgorithm() { + return "test"; + } + + @Override + public String getFormat() { + return "RAW"; + } + + @Override + public byte[] getEncoded() { + encodedCalled = true; + return SECRET.getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + @Override + public String toString() { + toStringCalled = true; + return SECRET; + } + } + + private static final class PlainKey implements Key { + private static final long serialVersionUID = 1L; + + @Override + public String getAlgorithm() { + return "plain"; + } + + @Override + public String getFormat() { + return null; + } + + @Override + public byte[] getEncoded() { + return null; + } + } + + private static final class BlockingKey implements Key, Destroyable { + private static final long serialVersionUID = 1L; + private final CountDownLatch destroyEntered = new CountDownLatch(1); + private final CountDownLatch allowDestroy = new CountDownLatch(1); + private final AtomicInteger destroyCalls = new AtomicInteger(); + private boolean destroyed; + + @Override + public void destroy() throws DestroyFailedException { + destroyCalls.incrementAndGet(); + destroyEntered.countDown(); + try { + allowDestroy.await(); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new DestroyFailedException("Interrupted while testing destruction"); + } + destroyed = true; + } + + @Override + public boolean isDestroyed() { + return destroyed; + } + + @Override + public String getAlgorithm() { + return "test"; + } + + @Override + public String getFormat() { + return null; + } + + @Override + public byte[] getEncoded() { + return null; + } + } + +} diff --git a/lib/src/test/java/zeroecho/sdk/builders/HybridKexBuilderTest.java b/lib/src/test/java/zeroecho/sdk/builders/HybridKexBuilderTest.java index 8852718..4823972 100644 --- a/lib/src/test/java/zeroecho/sdk/builders/HybridKexBuilderTest.java +++ b/lib/src/test/java/zeroecho/sdk/builders/HybridKexBuilderTest.java @@ -43,15 +43,20 @@ import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.security.KeyPair; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.audit.AuditListener; +import zeroecho.core.audit.AuditMode; import zeroecho.core.alg.common.agreement.KeyPairKey; import zeroecho.core.alg.kyber.KyberKeyGenSpec; import zeroecho.core.alg.xdh.XdhSpec; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.sdk.hybrid.kex.HybridKexContext; +import zeroecho.sdk.hybrid.kex.HybridKexContexts; import zeroecho.sdk.hybrid.kex.HybridKexPolicy; import zeroecho.sdk.hybrid.kex.HybridKexProfile; import zeroecho.sdk.hybrid.kex.HybridKexTranscript; @@ -87,20 +92,20 @@ class HybridKexBuilderTest { HybridKexTranscript transcript = new HybridKexTranscript().addUtf8("suite", "X25519+ML-KEM-768").addUtf8("role", "builder-test"); - KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); HybridKexContext alice = null; HybridKexContext bob = null; try { - alice = HybridKexBuilder.builder().profile(profile).transcript(transcript).classicAgreement() + alice = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).transcript(transcript).classicAgreement() .algorithm("Xdh").spec(XdhSpec.X25519).privateKey(aliceClassic.getPrivate()) .peerPublic(bobClassic.getPublic()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic()) .buildInitiator(); - bob = HybridKexBuilder.builder().profile(profile).transcript(transcript).classicAgreement().algorithm("Xdh") + bob = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).transcript(transcript).classicAgreement().algorithm("Xdh") .spec(XdhSpec.X25519).privateKey(bobClassic.getPrivate()).peerPublic(aliceClassic.getPublic()) .pqcKem().algorithm("ML-KEM").privateKey(bobPqc.getPrivate()).buildResponder(); @@ -133,19 +138,19 @@ class HybridKexBuilderTest { HybridKexProfile profile = HybridKexProfile.defaultProfile(32); - KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); HybridKexContext alice = null; HybridKexContext bob = null; try { - alice = HybridKexBuilder.builder().profile(profile).classicPairMessage().algorithm("Xdh") + alice = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).classicPairMessage().algorithm("Xdh") .spec(XdhSpec.X25519).keyPair(new KeyPairKey(aliceClassic)).pqcKem().algorithm("ML-KEM") .peerPublic(bobPqc.getPublic()).buildInitiator(); - bob = HybridKexBuilder.builder().profile(profile).classicPairMessage().algorithm("Xdh").spec(XdhSpec.X25519) + bob = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).classicPairMessage().algorithm("Xdh").spec(XdhSpec.X25519) .keyPair(new KeyPairKey(bobClassic)).pqcKem().algorithm("ML-KEM").privateKey(bobPqc.getPrivate()) .buildResponder(); @@ -179,12 +184,12 @@ class HybridKexBuilderTest { void buildInitiatorWithoutProfileFails() throws Exception { System.out.println("buildInitiatorWithoutProfileFails"); - KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); IllegalStateException exception = assertThrows(IllegalStateException.class, () -> { - HybridKexBuilder.builder().classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519) + HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519) .privateKey(aliceClassic.getPrivate()).peerPublic(bobClassic.getPublic()).pqcKem() .algorithm("ML-KEM").peerPublic(bobPqc.getPublic()).buildInitiator(); }); @@ -200,10 +205,10 @@ class HybridKexBuilderTest { System.out.println("buildInitiatorWithoutClassicModeFails"); HybridKexProfile profile = HybridKexProfile.defaultProfile(32); - KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); IllegalStateException exception = assertThrows(IllegalStateException.class, () -> { - HybridKexBuilder.builder().profile(profile).pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic()) + HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic()) .buildInitiator(); }); @@ -218,11 +223,11 @@ class HybridKexBuilderTest { System.out.println("buildInitiatorClassicAgreementWithoutPeerPublicFails"); HybridKexProfile profile = HybridKexProfile.defaultProfile(32); - KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); IllegalStateException exception = assertThrows(IllegalStateException.class, () -> { - HybridKexBuilder.builder().profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519) + HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519) .privateKey(aliceClassic.getPrivate()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic()) .buildInitiator(); }); @@ -238,10 +243,10 @@ class HybridKexBuilderTest { System.out.println("buildResponderPairMessageWithoutKeyPairFails"); HybridKexProfile profile = HybridKexProfile.defaultProfile(32); - KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); IllegalStateException exception = assertThrows(IllegalStateException.class, () -> { - HybridKexBuilder.builder().profile(profile).classicPairMessage().algorithm("Xdh").spec(XdhSpec.X25519) + HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).classicPairMessage().algorithm("Xdh").spec(XdhSpec.X25519) .pqcKem().algorithm("ML-KEM").privateKey(bobPqc.getPrivate()).buildResponder(); }); @@ -256,11 +261,11 @@ class HybridKexBuilderTest { System.out.println("buildInitiatorWithoutPqcPeerPublicFails"); HybridKexProfile profile = HybridKexProfile.defaultProfile(32); - KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); IllegalStateException exception = assertThrows(IllegalStateException.class, () -> { - HybridKexBuilder.builder().profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519) + HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519) .privateKey(aliceClassic.getPrivate()).peerPublic(bobClassic.getPublic()).pqcKem() .algorithm("ML-KEM").buildInitiator(); }); @@ -276,11 +281,11 @@ class HybridKexBuilderTest { System.out.println("buildResponderWithoutPqcPrivateFails"); HybridKexProfile profile = HybridKexProfile.defaultProfile(32); - KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); IllegalStateException exception = assertThrows(IllegalStateException.class, () -> { - HybridKexBuilder.builder().profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519) + HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519) .privateKey(bobClassic.getPrivate()).peerPublic(aliceClassic.getPublic()).pqcKem() .algorithm("ML-KEM").buildResponder(); }); @@ -298,12 +303,12 @@ class HybridKexBuilderTest { HybridKexProfile profile = HybridKexProfile.defaultProfile(16); HybridKexPolicy policy = new HybridKexPolicy(0, 0, 32); - KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - HybridKexBuilder.builder().profile(profile).policy(policy).classicAgreement().algorithm("Xdh") + HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).policy(policy).classicAgreement().algorithm("Xdh") .spec(XdhSpec.X25519).privateKey(aliceClassic.getPrivate()).peerPublic(bobClassic.getPublic()) .pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic()).buildInitiator(); }); @@ -314,18 +319,111 @@ class HybridKexBuilderTest { System.out.println("buildInitiatorRejectsPolicyWhenOkmTooShort...ok"); } + @Test + void policyFailureClosesBothConstructedLegs() throws Exception { + System.out.println("policyFailureClosesBothConstructedLegs"); + AtomicInteger closedContexts = new AtomicInteger(); + AuditListener listener = new AuditListener() { + @Override + public void onContextClosed(String contextId, long bodyBytes, long trailerBytes, long durationMillis) { + closedContexts.incrementAndGet(); + } + }; + ZeroEchoSession session = new ZeroEchoSession() + .withAuditListener(listener) + .withAuditMode(AuditMode.WRAP); + HybridKexProfile profile = HybridKexProfile.defaultProfile(16); + HybridKexPolicy policy = new HybridKexPolicy(0, 0, 32); + KeyPair aliceClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobPqc = session.keyBuilders().asymmetric().generateKeyPair("ML-KEM", + KyberKeyGenSpec.kyber768()); + + assertThrows(IllegalArgumentException.class, + () -> HybridKexBuilder.builder(session).profile(profile).policy(policy).classicAgreement() + .algorithm("Xdh").spec(XdhSpec.X25519).privateKey(aliceClassic.getPrivate()) + .peerPublic(bobClassic.getPublic()).pqcKem().algorithm("ML-KEM") + .peerPublic(bobPqc.getPublic()).buildInitiator()); + + assertEquals(2, closedContexts.get()); + System.out.println("...closedContexts=" + closedContexts.get()); + System.out.println("policyFailureClosesBothConstructedLegs...ok"); + } + + @Test + void secondLegPolicyFailureClosesFirstFactoryLeg() throws Exception { + System.out.println("secondLegPolicyFailureClosesFirstFactoryLeg"); + AtomicInteger closedContexts = new AtomicInteger(); + AuditListener listener = new AuditListener() { + @Override + public void onContextClosed(String contextId, long bodyBytes, long trailerBytes, long durationMillis) { + closedContexts.incrementAndGet(); + } + }; + ZeroEchoSession keySession = new ZeroEchoSession(); + KeyPair aliceClassic = keySession.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = keySession.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobPqc = keySession.keyBuilders().asymmetric().generateKeyPair("ML-KEM", + KyberKeyGenSpec.kyber768()); + ZeroEchoSession operationSession = new ZeroEchoSession() + .withAuditListener(listener) + .withAuditMode(AuditMode.WRAP) + .withPolicy((id, role, key, spec) -> { + if ("ML-KEM".equals(id)) { + throw new IllegalArgumentException("controlled PQ policy denial"); + } + }); + + assertThrows(IllegalArgumentException.class, + () -> HybridKexContexts.initiator(operationSession, HybridKexProfile.defaultProfile(32), + "Xdh", aliceClassic.getPrivate(), bobClassic.getPublic(), XdhSpec.X25519, + "ML-KEM", bobPqc.getPublic(), null)); + + assertEquals(1, closedContexts.get()); + System.out.println("...closedContexts=" + closedContexts.get()); + System.out.println("secondLegPolicyFailureClosesFirstFactoryLeg...ok"); + } + + @Test + void invalidFactoryInputIsRejectedBeforeContextAllocation() throws Exception { + System.out.println("invalidFactoryInputIsRejectedBeforeContextAllocation"); + AtomicInteger createdContexts = new AtomicInteger(); + AuditListener listener = new AuditListener() { + @Override + public void onContextCreatedMeta(String contextId, String algorithmId, String provider, + zeroecho.core.KeyUsage role, String keyFingerprint, + java.util.Map specMeta) { + createdContexts.incrementAndGet(); + } + }; + ZeroEchoSession session = new ZeroEchoSession() + .withAuditListener(listener) + .withAuditMode(AuditMode.WRAP); + KeyPair aliceClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + + assertThrows(NullPointerException.class, + () -> HybridKexContexts.initiator(session, HybridKexProfile.defaultProfile(32), + "Xdh", aliceClassic.getPrivate(), bobClassic.getPublic(), XdhSpec.X25519, + "ML-KEM", null, null)); + + assertEquals(0, createdContexts.get()); + System.out.println("...createdContexts=0"); + System.out.println("invalidFactoryInputIsRejectedBeforeContextAllocation...ok"); + } + @Test void switchingClassicModeClearsConflictingStateAndBuildsPairMessage() throws Exception { System.out.println("switchingClassicModeClearsConflictingStateAndBuildsPairMessage"); HybridKexProfile profile = HybridKexProfile.defaultProfile(32); - KeyPair agreementKeyPair = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair pairMessageKeyPair = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair agreementKeyPair = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair pairMessageKeyPair = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); HybridKexContext context = null; try { - HybridKexBuilder builder = HybridKexBuilder.builder().profile(profile); + HybridKexBuilder builder = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile); builder.classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519).privateKey(agreementKeyPair.getPrivate()) .peerPublic(agreementKeyPair.getPublic()); @@ -352,13 +450,13 @@ class HybridKexBuilderTest { HybridKexProfile profile = HybridKexProfile.defaultProfile(32); - KeyPair aliceClassicA = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobClassicA = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobPqcA = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair aliceClassicA = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassicA = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobPqcA = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); - KeyPair aliceClassicB = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobClassicB = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobPqcB = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair aliceClassicB = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassicB = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobPqcB = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); HybridKexTranscript transcriptA = new HybridKexTranscript().addUtf8("context", "A"); HybridKexTranscript transcriptB = new HybridKexTranscript().addUtf8("context", "B"); @@ -372,12 +470,12 @@ class HybridKexBuilderTest { HybridKexContext bobB = null; try { - aliceA = HybridKexBuilder.builder().profile(profile).transcript(transcriptA).classicAgreement() + aliceA = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).transcript(transcriptA).classicAgreement() .algorithm("Xdh").spec(XdhSpec.X25519).privateKey(aliceClassicA.getPrivate()) .peerPublic(bobClassicA.getPublic()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqcA.getPublic()) .buildInitiator(); - bobA = HybridKexBuilder.builder().profile(profile).transcript(transcriptA).classicAgreement() + bobA = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).transcript(transcriptA).classicAgreement() .algorithm("Xdh").spec(XdhSpec.X25519).privateKey(bobClassicA.getPrivate()) .peerPublic(aliceClassicA.getPublic()).pqcKem().algorithm("ML-KEM").privateKey(bobPqcA.getPrivate()) .buildResponder(); @@ -389,12 +487,12 @@ class HybridKexBuilderTest { System.out.println("...responderA=" + hex(responderA)); assertArrayEquals(secretA, responderA); - aliceB = HybridKexBuilder.builder().profile(profile).transcript(transcriptB).classicAgreement() + aliceB = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).transcript(transcriptB).classicAgreement() .algorithm("Xdh").spec(XdhSpec.X25519).privateKey(aliceClassicB.getPrivate()) .peerPublic(bobClassicB.getPublic()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqcB.getPublic()) .buildInitiator(); - bobB = HybridKexBuilder.builder().profile(profile).transcript(transcriptB).classicAgreement() + bobB = HybridKexBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).profile(profile).transcript(transcriptB).classicAgreement() .algorithm("Xdh").spec(XdhSpec.X25519).privateKey(bobClassicB.getPrivate()) .peerPublic(aliceClassicB.getPublic()).pqcKem().algorithm("ML-KEM").privateKey(bobPqcB.getPrivate()) .buildResponder(); @@ -468,4 +566,4 @@ class HybridKexBuilderTest { return "classicLen=?, pqcLen=?"; } } -} \ No newline at end of file +} diff --git a/lib/src/test/java/zeroecho/sdk/builders/TagTrailerDataContentBuilderTest.java b/lib/src/test/java/zeroecho/sdk/builders/TagTrailerDataContentBuilderTest.java index 004853b..2202908 100644 --- a/lib/src/test/java/zeroecho/sdk/builders/TagTrailerDataContentBuilderTest.java +++ b/lib/src/test/java/zeroecho/sdk/builders/TagTrailerDataContentBuilderTest.java @@ -63,8 +63,10 @@ import zeroecho.core.alg.sphincsplus.SphincsPlusKeyGenSpec; import zeroecho.core.context.EncryptionContext; import zeroecho.core.context.KemContext; import zeroecho.core.spec.AlgorithmKeySpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; -import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.core.KeyOperation; +import zeroecho.core.KeyOperationInfo; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.SymmetricKeyGenerator; import zeroecho.core.tag.TagEngine; import zeroecho.core.tag.TagEngineBuilder; import zeroecho.sdk.builders.alg.AesDataContentBuilder; @@ -173,26 +175,26 @@ public class TagTrailerDataContentBuilderTest { System.out.println("...msg=" + msg.length + " bytes"); // AES key - SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class) + SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyGenerator(AesKeyGenSpec.class) .generateSecret(AesKeyGenSpec.aes256()); // Ed25519 keys (JCA) - KeyPair ed = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec()); + KeyPair ed = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec()); - TagEngine tagEnc = TagEngineBuilder.ed25519Sign(ed.getPrivate()).get(); - TagEngine tagDec = TagEngineBuilder.ed25519Verify(ed.getPublic()).get(); + TagEngine tagEnc = TagEngineBuilder.ed25519Sign(new zeroecho.sdk.ZeroEchoSession(), ed.getPrivate()).get(); + TagEngine tagDec = TagEngineBuilder.ed25519Verify(new zeroecho.sdk.ZeroEchoSession(), ed.getPublic()).get(); // ENCRYPT: body -> [body||signature] -> AES-GCM DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) .add(new TagTrailerDataContentBuilder<>(tagEnc).bufferSize(8192)) - .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()).build(); + .add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader()).build(); byte[] ct = readAll(enc.getStream()); System.out.println("...ct=" + ct.length + " bytes"); // DECRYPT: AES-GCM -> strip trailer -> verify Ed25519 at EOF DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ct)) - .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()) + .add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader()) .add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build(); byte[] pt = readAll(dec.getStream()); @@ -210,28 +212,28 @@ public class TagTrailerDataContentBuilderTest { System.out.println("...msg=" + msg.length + " bytes"); // AES key - SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class) + SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyGenerator(AesKeyGenSpec.class) .generateSecret(AesKeyGenSpec.aes256()); // SPHINCS+ key pair via registry (uses default param set from // SphincsPlusKeyGenSpec) - KeyPair spx = CryptoAlgorithms.keyPair("SPHINCS+", SphincsPlusKeyGenSpec.defaultSpec()); + KeyPair spx = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("SPHINCS+", SphincsPlusKeyGenSpec.defaultSpec()); // Tag engines (SPHINCS+) - TagEngine tagEnc = TagEngineBuilder.sphincsPlusSign(spx.getPrivate()).get(); - TagEngine tagDec = TagEngineBuilder.sphincsPlusVerify(spx.getPublic()).get(); + TagEngine tagEnc = TagEngineBuilder.sphincsPlusSign(new zeroecho.sdk.ZeroEchoSession(), spx.getPrivate()).get(); + TagEngine tagDec = TagEngineBuilder.sphincsPlusVerify(new zeroecho.sdk.ZeroEchoSession(), spx.getPublic()).get(); // ENCRYPT: body -> [body||spxSig] -> AES-GCM DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) .add(new TagTrailerDataContentBuilder<>(tagEnc).bufferSize(8192)) - .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()).build(); + .add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader()).build(); byte[] ct = readAll(enc.getStream()); System.out.println("...ct=" + ct.length + " bytes"); // DECRYPT: AES-GCM -> strip trailer -> verify SPHINCS+ at EOF DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ct)) - .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()) + .add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader()) .add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build(); byte[] pt = readAll(dec.getStream()); @@ -249,28 +251,28 @@ public class TagTrailerDataContentBuilderTest { System.out.println("...msg=" + msg.length + " bytes"); // AES key - SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class) + SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyGenerator(AesKeyGenSpec.class) .generateSecret(AesKeyGenSpec.aes256()); // RSA-2048 keys (use registry for convenience) - KeyPair rsa = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048()); + KeyPair rsa = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048()); // Tag engines (SHA-256, saltLen=32) RsaSigSpec pss = RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32); - TagEngine tagEnc = TagEngineBuilder.rsaSign(rsa.getPrivate(), pss).get(); - TagEngine tagDec = TagEngineBuilder.rsaVerify(rsa.getPublic(), pss).get(); + TagEngine tagEnc = TagEngineBuilder.rsaSign(new zeroecho.sdk.ZeroEchoSession(), rsa.getPrivate(), pss).get(); + TagEngine tagDec = TagEngineBuilder.rsaVerify(new zeroecho.sdk.ZeroEchoSession(), rsa.getPublic(), pss).get(); // ENCRYPT: body -> [body||pssSig] -> AES-GCM DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) .add(new TagTrailerDataContentBuilder<>(tagEnc).bufferSize(8192)) - .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()).build(); + .add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader()).build(); byte[] ct = readAll(enc.getStream()); System.out.println("...ct=" + ct.length + " bytes"); // DECRYPT: AES-GCM -> strip trailer -> verify PSS at EOF DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ct)) - .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()) + .add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader()) .add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build(); byte[] pt = readAll(dec.getStream()); @@ -289,14 +291,14 @@ public class TagTrailerDataContentBuilderTest { System.out.println("...msg=" + msg.length + " bytes"); // AES key up-front so decrypt can reuse it - SymmetricKeyBuilder gen = CryptoAlgorithms.require("AES") - .symmetricKeyBuilder(AesKeyGenSpec.class); + SymmetricKeyGenerator gen = CryptoAlgorithms.require("AES") + .symmetricKeyGenerator(AesKeyGenSpec.class); SecretKey aesKey = gen.generateSecret(AesKeyGenSpec.aes256()); // ENCRYPT: [source] -> [tag trailer] -> [aes gcm] DataContent encChain = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) - .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192)) - .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()) // writes IV/AAD headers + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192)) + .add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader()) // writes IV/AAD headers // into stream .build(); @@ -305,9 +307,9 @@ public class TagTrailerDataContentBuilderTest { // DECRYPT: [source(ct)] -> [aes gcm] -> [tag trailer verify] DataContent decChain = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext)) - .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()) // reads IV/AAD headers + .add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader()) // reads IV/AAD headers // back - .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192) + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192) .throwOnMismatch()) .build(); @@ -328,12 +330,12 @@ public class TagTrailerDataContentBuilderTest { msg = Arrays.copyOf(msg, SIZE); // pad deterministic length for the test System.out.println("...msg=" + msg.length + " bytes"); - KeyPair kp = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048()); + KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048()); // ENCRYPT: [source] -> [tag trailer] -> [rsa/oaep] DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) - .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192)) - .add(RsaEncDataContentBuilder.builder().oaep(RsaEncSpec.Hash.SHA256).withPublicKey(kp.getPublic())) + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192)) + .add(RsaEncDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).oaep(RsaEncSpec.Hash.SHA256).withPublicKey(kp.getPublic())) .build(); byte[] ct = readAll(enc.getStream()); @@ -341,8 +343,8 @@ public class TagTrailerDataContentBuilderTest { // DECRYPT: [source(ct)] -> [rsa/oaep] -> [tag verify] DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ct)) - .add(RsaEncDataContentBuilder.builder().oaep(RsaEncSpec.Hash.SHA256).withPrivateKey(kp.getPrivate())) - .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192) + .add(RsaEncDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).oaep(RsaEncSpec.Hash.SHA256).withPrivateKey(kp.getPrivate())) + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192) .throwOnMismatch()) .build(); @@ -377,12 +379,12 @@ public class TagTrailerDataContentBuilderTest { System.out.println("...msg=" + msg.length + " bytes"); // ENCRYPT: [source] -> [tag trailer] -> [KEM envelope with AES/GCM payload] - AesDataContentBuilder aesEnc = AesDataContentBuilder.builder().modeGcm(128) // 128-bit tag + AesDataContentBuilder aesEnc = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128) // 128-bit tag .withHeader(); // carry IV etc. DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) - .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192)) - .add(KemDataContentBuilder.builder().kem(kemId).recipientPublic(kemKeys.getPublic()).derivedKeyBytes(32) // AES-256 + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192)) + .add(KemDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).kem(kemId).recipientPublic(kemKeys.getPublic()).derivedKeyBytes(32) // AES-256 // key // derived // from @@ -396,14 +398,14 @@ public class TagTrailerDataContentBuilderTest { System.out.println("...envelope=" + envelope.length + " bytes"); // DECRYPT: [source(envelope)] -> [KEM] -> [tag verify] - AesDataContentBuilder aesDec = AesDataContentBuilder.builder().modeGcm(128).withHeader(); + AesDataContentBuilder aesDec = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader(); DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(envelope)) - .add(KemDataContentBuilder.builder().kem(kemId).recipientPrivate(kemKeys.getPrivate()) + .add(KemDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).kem(kemId).recipientPrivate(kemKeys.getPrivate()) .derivedKeyBytes(32) .hkdfSha256("KEM-tag-demo".getBytes(java.nio.charset.StandardCharsets.US_ASCII)) .withAes(aesDec)) - .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192) + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192) .throwOnMismatch()) .build(); @@ -428,26 +430,26 @@ public class TagTrailerDataContentBuilderTest { // --- recipients --- // RSA - KeyPair rsa = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048()); + KeyPair rsa = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048()); // ML-KEM (Kyber768 as a good mid-level) - KeyPair kem = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair kem = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); // --- symmetric payload (AES-256/GCM, tag 128) --- // IV length is handled internally (12 bytes for GCM) and persisted via header. - AesDataContentBuilder aesEnc = AesDataContentBuilder.builder().modeGcm(128).withHeader(); // write + AesDataContentBuilder aesEnc = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader(); // write // IV/tagBits/AAD-hash // header for decrypt // side // --- tag trailer (SHA-256 digest as a trailer) --- TagTrailerDataContentBuilder tagEnc = new TagTrailerDataContentBuilder<>( - TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192); + TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192); - EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); - KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kem.getPublic()); + EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + KemContext kybKem = new zeroecho.sdk.ZeroEchoSession().createContext("ML-KEM", KeyUsage.ENCAPSULATE, kem.getPublic()); // --- envelope (ENCRYPT) with 3 recipients --- - MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesEnc) + MultiRecipientDataSourceBuilder envEnc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesEnc) // .addRsaOaepRecipient(rsa.getPublic()) // RSA-OAEP with SHA-256 MGF1 // .addKemRecipient("ML-KEM", kem.getPublic(), 32 /* kekBytes */, 16 /* // hkdfSaltLen */) @@ -465,35 +467,35 @@ public class TagTrailerDataContentBuilderTest { // -------------- Decrypt three ways on the same ciphertext -------------- // a) by RSA private key - AesDataContentBuilder aesDecRsa = AesDataContentBuilder.builder().modeGcm(128).withHeader(); // read header to + AesDataContentBuilder aesDecRsa = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader(); // read header to // recover // IV/tagBits - MultiRecipientDataSourceBuilder envDecRsa = new MultiRecipientDataSourceBuilder().withAes(aesDecRsa) + MultiRecipientDataSourceBuilder envDecRsa = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesDecRsa) .unlockWith(new UnlockMaterial.Private(rsa.getPrivate())); byte[] ptRsa = readAll(DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(encrypted)).add(envDecRsa) - .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192) + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192) .throwOnMismatch()) .build().getStream()); System.out.println("...decrypted(RSA)=" + ptRsa.length); assertArrayEquals(msg, ptRsa, "RSA path failed to recover the original"); // b) by KEM private key - AesDataContentBuilder aesDecKem = AesDataContentBuilder.builder().modeGcm(128).withHeader(); - MultiRecipientDataSourceBuilder envDecKem = new MultiRecipientDataSourceBuilder().withAes(aesDecKem) + AesDataContentBuilder aesDecKem = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader(); + MultiRecipientDataSourceBuilder envDecKem = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesDecKem) .unlockWith(new UnlockMaterial.Private(kem.getPrivate())); byte[] ptKem = readAll(DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(encrypted)).add(envDecKem) - .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192) + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192) .throwOnMismatch()) .build().getStream()); System.out.println("...decrypted(KEM)=" + ptKem.length); assertArrayEquals(msg, ptKem, "KEM path failed to recover the original"); // c) by password - AesDataContentBuilder aesDecPwd = AesDataContentBuilder.builder().modeGcm(128).withHeader(); - MultiRecipientDataSourceBuilder envDecPwd = new MultiRecipientDataSourceBuilder().withAes(aesDecPwd) + AesDataContentBuilder aesDecPwd = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader(); + MultiRecipientDataSourceBuilder envDecPwd = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesDecPwd) .unlockWith(new UnlockMaterial.Password(PASSWORD)); byte[] ptPwd = readAll(DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(encrypted)).add(envDecPwd) - .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192) + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192) .throwOnMismatch()) .build().getStream()); System.out.println("...decrypted(PASSWORD)=" + ptPwd.length); @@ -514,23 +516,23 @@ public class TagTrailerDataContentBuilderTest { byte[] msg = random(SIZE); System.out.println("...input=" + msg.length); - KeyPair rsa = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048()); + KeyPair rsa = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048()); // AES-256/CBC with header so IV/params are serialized by the AES stage - AesDataContentBuilder aesCbc = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + AesDataContentBuilder aesCbc = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5().withHeader(); TagTrailerDataContentBuilder tagEnc = new TagTrailerDataContentBuilder<>( - TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192); + TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192); TagTrailerDataContentBuilder tagDec = new TagTrailerDataContentBuilder<>( - TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192) + TagEngineBuilder.digest(new zeroecho.sdk.ZeroEchoSession(), DigestSpec.sha256())).bufferSize(8192) // explicit for clarity .throwOnMismatch(); - EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); // Envelope: recipient table (RSA-OAEP) + AES payload (CBC/PKCS7 with header) - MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesCbc) + MultiRecipientDataSourceBuilder envEnc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc) // CEK length for AES-256 .payloadKeyBytes(32) // .addRsaOaepRecipient(rsa.getPublic()); old API @@ -543,8 +545,8 @@ public class TagTrailerDataContentBuilderTest { byte[] encrypted = readAll(encTail.getStream()); System.out.println("...encrypted=" + encrypted.length); - MultiRecipientDataSourceBuilder envDec = new MultiRecipientDataSourceBuilder() - .withAes(AesDataContentBuilder.builder().modeCbcPkcs5() + MultiRecipientDataSourceBuilder envDec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))) + .withAes(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5() // must match encrypt side .withHeader()) .payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(rsa.getPrivate())); @@ -571,27 +573,27 @@ public class TagTrailerDataContentBuilderTest { System.out.println("...msg=" + msg.length + " bytes"); // AES key - SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class) + SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyGenerator(AesKeyGenSpec.class) .generateSecret(AesKeyGenSpec.aes256()); // ECDSA P-256 keys (via your unified ECDSA algorithm) - KeyPair ecdsa = CryptoAlgorithms.keyPair("ECDSA", zeroecho.core.alg.ecdsa.EcdsaCurveSpec.P256); + KeyPair ecdsa = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ECDSA", zeroecho.core.alg.ecdsa.EcdsaCurveSpec.P256); // Tag engines (ECDSA/P-256 using P1363 format, fixed 64-byte tag) - TagEngine tagEnc = TagEngineBuilder.ecdsaP256Sign(ecdsa.getPrivate()).get(); - TagEngine tagDec = TagEngineBuilder.ecdsaP256Verify(ecdsa.getPublic()).get(); + TagEngine tagEnc = TagEngineBuilder.ecdsaP256Sign(new zeroecho.sdk.ZeroEchoSession(), ecdsa.getPrivate()).get(); + TagEngine tagDec = TagEngineBuilder.ecdsaP256Verify(new zeroecho.sdk.ZeroEchoSession(), ecdsa.getPublic()).get(); // ENCRYPT: body -> [body||ecdsaSig] -> AES-GCM DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) .add(new TagTrailerDataContentBuilder<>(tagEnc).bufferSize(8192)) - .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()).build(); + .add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader()).build(); byte[] ct = readAll(enc.getStream()); System.out.println("...ct=" + ct.length + " bytes"); // DECRYPT: AES-GCM -> strip trailer -> verify ECDSA at EOF DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ct)) - .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()) + .add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withKey(aesKey).modeGcm(128).withHeader()) .add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build(); byte[] pt = readAll(dec.getStream()); @@ -612,14 +614,14 @@ public class TagTrailerDataContentBuilderTest { private static KeyPair tryKeyPairWithDefaultSpec(CryptoAlgorithm alg) { try { - for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) { - if (bi.defaultKeySpec == null) { + for (KeyOperationInfo bi : alg.keyOperations()) { + if (bi.operation() != KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE || bi.defaultSpec() == null) { continue; } @SuppressWarnings("unchecked") - Class specType = (Class) bi.specType; - AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec; - AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(specType); + Class specType = (Class) bi.specType(); + AlgorithmKeySpec spec = bi.defaultSpec(); + AsymmetricKeyPairGenerator b = alg.asymmetricKeyPairGenerator(specType); KeyPair kp = b.generateKeyPair(spec); if (kp != null) { return kp; diff --git a/lib/src/test/java/zeroecho/sdk/builders/alg/KemHybridRoundTripTest.java b/lib/src/test/java/zeroecho/sdk/builders/alg/KemHybridRoundTripTest.java index 1336d35..3f7e49f 100644 --- a/lib/src/test/java/zeroecho/sdk/builders/alg/KemHybridRoundTripTest.java +++ b/lib/src/test/java/zeroecho/sdk/builders/alg/KemHybridRoundTripTest.java @@ -218,7 +218,7 @@ class KemHybridRoundTripTest { new Random(123456789L).nextBytes(input); // keypair via generic registry path - KeyPair kp = CryptoAlgorithms.keyPair(kemId, keyGenSpec); + KeyPair kp = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair(kemId, keyGenSpec); // encrypt DataContent enc = encryptStage(kemId, kp, mode); @@ -244,24 +244,24 @@ class KemHybridRoundTripTest { } private static DataContent encryptStage(String kemId, KeyPair kp, String mode) { - KemDataContentBuilder kem = KemDataContentBuilder.builder().kem(kemId).recipientPublic(kp.getPublic()) + KemDataContentBuilder kem = KemDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).kem(kemId).recipientPublic(kp.getPublic()) .derivedKeyBytes(32); // AES-256 or ChaCha20 key switch (mode) { case "GCM": { - AesDataContentBuilder aes = AesDataContentBuilder.builder().modeGcm(128).withHeader().withAad(AAD); + AesDataContentBuilder aes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader().withAad(AAD); return kem.withAes(aes).build(true); } case "CBC": { - AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + AesDataContentBuilder aes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5().withHeader(); return kem.withAes(aes).build(true); } case "CTR": { - AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCtr().withHeader(); + AesDataContentBuilder aes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCtr().withHeader(); return kem.withAes(aes).build(true); } case "CHACHA20-POLY1305": { - ChaChaDataContentBuilder ch = ChaChaDataContentBuilder.builder().withAad(AAD) // non-empty → AEAD + ChaChaDataContentBuilder ch = ChaChaDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withAad(AAD) // non-empty → AEAD // variant .withHeader(); // carry nonce return kem.withChaCha(ch).build(true); @@ -272,24 +272,24 @@ class KemHybridRoundTripTest { } private static DataContent decryptStage(String kemId, KeyPair kp, String mode) { - KemDataContentBuilder kem = KemDataContentBuilder.builder().kem(kemId).recipientPrivate(kp.getPrivate()) + KemDataContentBuilder kem = KemDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).kem(kemId).recipientPrivate(kp.getPrivate()) .derivedKeyBytes(32); switch (mode) { case "GCM": { - AesDataContentBuilder aes = AesDataContentBuilder.builder().modeGcm(128).withHeader().withAad(AAD); + AesDataContentBuilder aes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader().withAad(AAD); return kem.withAes(aes).build(false); } case "CBC": { - AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + AesDataContentBuilder aes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5().withHeader(); return kem.withAes(aes).build(false); } case "CTR": { - AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCtr().withHeader(); + AesDataContentBuilder aes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCtr().withHeader(); return kem.withAes(aes).build(false); } case "CHACHA20-POLY1305": { - ChaChaDataContentBuilder ch = ChaChaDataContentBuilder.builder().withAad(AAD).withHeader(); + ChaChaDataContentBuilder ch = ChaChaDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withAad(AAD).withHeader(); return kem.withChaCha(ch).build(false); } default: diff --git a/lib/src/test/java/zeroecho/sdk/builders/alg/SessionBoundBuilderTest.java b/lib/src/test/java/zeroecho/sdk/builders/alg/SessionBoundBuilderTest.java new file mode 100644 index 0000000..e27827e --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/builders/alg/SessionBoundBuilderTest.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.sdk.builders.alg; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.InputStream; +import java.security.Key; +import java.util.Map; + +import javax.crypto.spec.SecretKeySpec; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.KeyUsage; +import zeroecho.core.audit.AuditListener; +import zeroecho.core.audit.AuditMode; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.sdk.ZeroEchoSession; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.builtin.PlainBytes; + +class SessionBoundBuilderTest { + + @Test + void generatedKeyUsesBuilderSessionAuditSink() { + System.out.println("generatedKeyUsesBuilderSessionAuditSink"); + RecordingListener listener = new RecordingListener(); + ZeroEchoSession session = new ZeroEchoSession().withAuditListener(listener); + + AesDataContentBuilder.builder(session).generateKey(128).modeGcm(128).build(true); + + assertEquals(1, listener.keyBuilt); + System.out.println("...keyBuiltEvents=" + listener.keyBuilt); + System.out.println("generatedKeyUsesBuilderSessionAuditSink...ok"); + } + + @Test + void policyDenialOccursBeforeContextCreation() { + System.out.println("policyDenialOccursBeforeContextCreation"); + RecordingListener listener = new RecordingListener(); + ZeroEchoSession session = new ZeroEchoSession() + .withAuditListener(listener) + .withPolicy((id, role, key, spec) -> { + throw new IllegalArgumentException("controlled policy denial"); + }); + DataContent encryption = AesDataContentBuilder.builder(session) + .withKey(new SecretKeySpec(new byte[16], "AES")) + .modeGcm(128) + .build(true); + encryption.setInput(new PlainBytes(new byte[] { 1 })); + + assertThrows(IllegalArgumentException.class, encryption::getStream); + assertEquals(0, listener.contextCreated); + System.out.println("...contextCreatedEvents=0"); + System.out.println("policyDenialOccursBeforeContextCreation...ok"); + } + + @Test + void wrappedContextUsesBuilderSessionAuditConfiguration() throws Exception { + System.out.println("wrappedContextUsesBuilderSessionAuditConfiguration"); + RecordingListener listener = new RecordingListener(); + ZeroEchoSession session = new ZeroEchoSession() + .withAuditListener(listener) + .withAuditMode(AuditMode.WRAP); + DataContent encryption = AesDataContentBuilder.builder(session) + .withKey(new SecretKeySpec(new byte[16], "AES")) + .modeGcm(128) + .build(true); + encryption.setInput(new PlainBytes(new byte[] { 1, 2, 3 })); + + try (InputStream input = encryption.getStream()) { + input.readAllBytes(); + } + + assertEquals(1, listener.contextCreated); + assertEquals(1, listener.contextClosed); + System.out.println("...created=1...closed=1"); + System.out.println("wrappedContextUsesBuilderSessionAuditConfiguration...ok"); + } + + private static final class RecordingListener implements AuditListener { + private int keyBuilt; + private int contextCreated; + private int contextClosed; + + @Override + public void onKeyBuilt(String id, String provider, AlgorithmKeySpec spec, Key key) { + keyBuilt++; + } + + @Override + public void onContextCreatedMeta(String contextId, String algorithmId, String provider, KeyUsage role, + String keyFingerprint, Map specMeta) { + contextCreated++; + } + + @Override + public void onContextClosed(String contextId, long bodyBytes, long trailerBytes, long durationMillis) { + contextClosed++; + } + } +} diff --git a/lib/src/test/java/zeroecho/sdk/content/builtin/SecretPasswordTest.java b/lib/src/test/java/zeroecho/sdk/content/builtin/SecretPasswordTest.java index 0678ee9..d07c352 100644 --- a/lib/src/test/java/zeroecho/sdk/content/builtin/SecretPasswordTest.java +++ b/lib/src/test/java/zeroecho/sdk/content/builtin/SecretPasswordTest.java @@ -33,22 +33,69 @@ ******************************************************************************/ package zeroecho.sdk.content.builtin; +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.io.InputStream; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import org.junit.jupiter.api.Test; class SecretPasswordTest { @Test - void testGetPlainText() { + void redactsDiagnosticsAndOwnsPasswordCharacters() { + System.out.print("SecretPassword/redaction..."); + char[] caller = "correct horse".toCharArray(); + char[] expected = caller.clone(); + SecretPassword password = new SecretPassword(caller); + Arrays.fill(caller, 'x'); - int len = 12; + assertEquals("[REDACTED]", password.toText()); + assertEquals("[REDACTED]", password.toString()); + assertArrayEquals(expected, password.chars()); + char[] copy = password.chars(); + copy[0] = 'x'; + assertArrayEquals(expected, password.chars()); + System.out.println("ok"); + } - System.out.printf("generating password, %d characters...%n", len); + @Test + void destroysOwnedCharactersAndRejectsSecretAccess() throws Exception { + System.out.print("SecretPassword/destroy..."); + SecretPassword password = new SecretPassword("sensitive".toCharArray()); + assertFalse(password.isDestroyed()); - SecretPassword sp = new SecretPassword(len); + password.destroy(); + password.destroy(); - System.out.printf("...string %s (length %d)%n", sp.toText(), sp.toText().length()); + assertTrue(password.isDestroyed()); + assertThrows(IllegalStateException.class, password::chars); + assertThrows(IllegalStateException.class, password::toBytes); + assertThrows(IllegalStateException.class, password::getStream); + Field field = SecretPassword.class.getDeclaredField("password"); + field.setAccessible(true); + char[] owned = (char[]) field.get(password); + assertTrue(Arrays.equals(new char[owned.length], owned)); + System.out.println("ok"); + } - assertEquals(len, sp.toText().length()); + @Test + void streamOwnsAndWipesItsUtf8BufferOnClose() throws Exception { + System.out.print("SecretPassword/stream..."); + SecretPassword password = new SecretPassword("päss".toCharArray()); + InputStream stream = password.getStream(); + assertArrayEquals("päss".getBytes(StandardCharsets.UTF_8), stream.readAllBytes()); + + Field field = stream.getClass().getDeclaredField("ownedBuffer"); + field.setAccessible(true); + byte[] buffer = (byte[]) field.get(stream); + stream.close(); + assertTrue(Arrays.equals(new byte[buffer.length], buffer)); + System.out.println("ok"); } } diff --git a/lib/src/test/java/zeroecho/sdk/guard/DecryptorCekCleanupTest.java b/lib/src/test/java/zeroecho/sdk/guard/DecryptorCekCleanupTest.java new file mode 100644 index 0000000..a855e8f --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/guard/DecryptorCekCleanupTest.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.sdk.guard; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.io.Util; +import zeroecho.sdk.ZeroEchoSession; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; + +class DecryptorCekCleanupTest { + + @Test + void rejectedOversizedCekIsCleared() throws Exception { + System.out.println("rejectedOversizedCekIsCleared"); + byte[] rejected = new byte[17]; + RecipientOpener opener = (entryId, entryBlob, material) -> rejected; + Decryptor decryptor = decryptor(opener); + decryptor.setInput(() -> new ByteArrayInputStream(envelopeHeader())); + + assertThrows(IOException.class, decryptor::getStream); + + assertArrayEquals(new byte[rejected.length], rejected); + System.out.println("...rejectedBytesCleared=17"); + System.out.println("rejectedOversizedCekIsCleared...ok"); + } + + @Test + void acceptedCekIsClearedWhenCiphertextHeaderIsTruncated() throws Exception { + System.out.println("acceptedCekIsClearedWhenCiphertextHeaderIsTruncated"); + byte[] accepted = new byte[16]; + RecipientOpener opener = (entryId, entryBlob, material) -> accepted; + Decryptor decryptor = decryptor(opener); + decryptor.setInput(() -> new ByteArrayInputStream(envelopeHeader())); + + assertThrows(IOException.class, decryptor::getStream); + assertArrayEquals(new byte[accepted.length], accepted); + + System.out.println("...acceptedBytesCleared=16"); + System.out.println("acceptedCekIsClearedWhenCiphertextHeaderIsTruncated...ok"); + } + + @Test + void acceptedCekIsClearedWhenPayloadSetupFails() throws Exception { + System.out.println("acceptedCekIsClearedWhenPayloadSetupFails"); + byte[] accepted = new byte[16]; + RecipientOpener opener = (entryId, entryBlob, material) -> accepted; + Decryptor decryptor = new Decryptor(List.of(opener), new UnlockMaterial.Password(new char[] { 'p' }), + null, null, 16, 4, 128); + decryptor.setInput(() -> new ByteArrayInputStream(envelopeHeader())); + + assertThrows(NullPointerException.class, decryptor::getStream); + + assertArrayEquals(new byte[accepted.length], accepted); + System.out.println("...failurePathCleared=true"); + System.out.println("acceptedCekIsClearedWhenPayloadSetupFails...ok"); + } + + @Test + void preReturnFailureClosesUpstreamStream() throws Exception { + System.out.println("preReturnFailureClosesUpstreamStream"); + ByteArrayOutputStream header = new ByteArrayOutputStream(); + Util.writePack7I(header, 5); + TrackingInputStream input = new TrackingInputStream(header.toByteArray(), false); + Decryptor decryptor = decryptor((entryId, entryBlob, material) -> null); + decryptor.setInput(() -> input); + + assertThrows(IOException.class, decryptor::getStream); + assertTrue(input.closed); + + System.out.println("...upstreamClosed=true"); + System.out.println("preReturnFailureClosesUpstreamStream...ok"); + } + + @Test + void cleanupFailureIsSuppressedOnPrimaryFailure() throws Exception { + System.out.println("cleanupFailureIsSuppressedOnPrimaryFailure"); + ByteArrayOutputStream header = new ByteArrayOutputStream(); + Util.writePack7I(header, 5); + TrackingInputStream input = new TrackingInputStream(header.toByteArray(), true); + Decryptor decryptor = decryptor((entryId, entryBlob, material) -> null); + decryptor.setInput(() -> input); + + IOException failure = assertThrows(IOException.class, decryptor::getStream); + assertEquals(1, failure.getSuppressed().length); + assertTrue(input.closed); + + System.out.println("...suppressedCleanupFailures=1"); + System.out.println("cleanupFailureIsSuppressedOnPrimaryFailure...ok"); + } + + private static Decryptor decryptor(RecipientOpener opener) { + AesDataContentBuilder aes = AesDataContentBuilder.builder(new ZeroEchoSession()).withHeader().modeGcm(128); + return new Decryptor(List.of(opener), new UnlockMaterial.Password(new char[] { 'p' }), + aes, null, 16, 4, 128); + } + + private static byte[] envelopeHeader() throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + Util.writePack7I(output, 1); + Util.writeUTF8(output, "controlled"); + Util.write(output, new byte[] { 1 }); + return output.toByteArray(); + } + + private static final class TrackingInputStream extends InputStream { + private final ByteArrayInputStream delegate; + private final boolean failClose; + private boolean closed; + + private TrackingInputStream(byte[] bytes, boolean failClose) { + this.delegate = new ByteArrayInputStream(bytes); + this.failClose = failClose; + } + + @Override + public int read() { + return delegate.read(); + } + + @Override + public void close() throws IOException { + closed = true; + if (failClose) { + throw new IOException("controlled close failure"); + } + delegate.close(); + } + } +} diff --git a/lib/src/test/java/zeroecho/sdk/guard/EncryptorCekAllocationTest.java b/lib/src/test/java/zeroecho/sdk/guard/EncryptorCekAllocationTest.java new file mode 100644 index 0000000..603459b --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/guard/EncryptorCekAllocationTest.java @@ -0,0 +1,218 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.sdk.guard; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.IntFunction; + +import org.junit.jupiter.api.Test; + +import zeroecho.sdk.Pbkdf2Limits; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; + +class EncryptorCekAllocationTest { + private static final int KEY_BYTES = 16; + + @Test + void noDecoysGenerateOnlyPayloadCek() throws Exception { + System.out.print("EncryptorCekAllocation/noDecoysGenerateOnlyPayloadCek..."); + RecordingRandomFactory randomFactory = new RecordingRandomFactory(); + CapturingRecipient first = new CapturingRecipient(false); + CapturingRecipient second = new CapturingRecipient(false); + + openEncryptor(List.of(first, second), randomFactory).close(); + + assertEquals(1, randomFactory.calls()); + assertArrayEquals(repeated((byte) 1), first.cek); + assertArrayEquals(first.cek, second.cek); + assertAllCleared(randomFactory.outputs); + System.out.println("...factoryCalls=" + randomFactory.calls()); + System.out.println("ok"); + } + + @Test + void oneDecoyGeneratesOneLazyDecoyCek() throws Exception { + System.out.print("EncryptorCekAllocation/oneDecoyGeneratesOneLazyDecoyCek..."); + RecordingRandomFactory randomFactory = new RecordingRandomFactory(); + CapturingRecipient decoy = new CapturingRecipient(true); + + openEncryptor(List.of(decoy), randomFactory).close(); + + assertEquals(2, randomFactory.calls()); + assertArrayEquals(repeated((byte) 2), decoy.cek); + assertAllCleared(randomFactory.outputs); + System.out.println("...factoryCalls=" + randomFactory.calls()); + System.out.println("ok"); + } + + @Test + void mixedDecoysGenerateOneLazyCekAndReuseItInOrder() throws Exception { + System.out.print("EncryptorCekAllocation/mixedDecoysGenerateOneLazyCekAndReuseItInOrder..."); + RecordingRandomFactory randomFactory = new RecordingRandomFactory(); + CapturingRecipient realBefore = new CapturingRecipient(false); + CapturingRecipient firstDecoy = new CapturingRecipient(true); + CapturingRecipient realAfter = new CapturingRecipient(false); + CapturingRecipient secondDecoy = new CapturingRecipient(true); + + openEncryptor(List.of(realBefore, firstDecoy, realAfter, secondDecoy), randomFactory).close(); + + assertEquals(2, randomFactory.calls()); + assertArrayEquals(repeated((byte) 1), realBefore.cek); + assertArrayEquals(realBefore.cek, realAfter.cek); + assertArrayEquals(repeated((byte) 2), firstDecoy.cek); + assertArrayEquals(firstDecoy.cek, secondDecoy.cek); + assertAllCleared(randomFactory.outputs); + System.out.println("...factoryCalls=" + randomFactory.calls()); + System.out.println("ok"); + } + + @Test + void recipientLimitIsCheckedBeforeCekGeneration() { + System.out.print("EncryptorCekAllocation/recipientLimitIsCheckedBeforeCekGeneration..."); + RecordingRandomFactory randomFactory = new RecordingRandomFactory(); + Encryptor encryptor = newEncryptor(List.of(new CapturingRecipient(false), new CapturingRecipient(false)), + 1, randomFactory); + encryptor.setInput(() -> new ByteArrayInputStream(new byte[0])); + + assertThrows(IOException.class, encryptor::getStream); + assertEquals(0, randomFactory.calls()); + System.out.println("...factoryCalls=0"); + System.out.println("ok"); + } + + @Test + void recipientFailureClearsGenuineAndDecoyCeks() { + System.out.print("EncryptorCekAllocation/recipientFailureClearsGenuineAndDecoyCeks..."); + RecordingRandomFactory randomFactory = new RecordingRandomFactory(); + Encryptor encryptor = newEncryptor(List.of(new CapturingRecipient(false), new FailingRecipient(true)), + 8, randomFactory); + encryptor.setInput(() -> new ByteArrayInputStream(new byte[0])); + + assertThrows(IOException.class, encryptor::getStream); + assertEquals(2, randomFactory.calls()); + assertAllCleared(randomFactory.outputs); + System.out.println("...factoryCalls=" + randomFactory.calls()); + System.out.println("ok"); + } + + @Test + void successfulRecipientProcessingDestroysOwnedPassword() throws Exception { + System.out.print("EncryptorCekAllocation/successfulRecipientProcessingDestroysOwnedPassword..."); + PasswordRecipient passwordRecipient = passwordRecipient(); + + openEncryptor(List.of(passwordRecipient), new RecordingRandomFactory()).close(); + + assertTrue(passwordRecipient.isDestroyed()); + System.out.println("...passwordDestroyed=true"); + System.out.println("ok"); + } + + @Test + void failedRecipientProcessingDestroysOwnedPassword() { + System.out.print("EncryptorCekAllocation/failedRecipientProcessingDestroysOwnedPassword..."); + PasswordRecipient passwordRecipient = passwordRecipient(); + Encryptor encryptor = newEncryptor(List.of(passwordRecipient, new FailingRecipient(false)), + 8, new RecordingRandomFactory()); + encryptor.setInput(() -> new ByteArrayInputStream(new byte[0])); + + assertThrows(IOException.class, encryptor::getStream); + assertTrue(passwordRecipient.isDestroyed()); + System.out.println("...passwordDestroyed=true"); + System.out.println("ok"); + } + + private static InputStream openEncryptor(List recipients, IntFunction randomFactory) + throws IOException { + Encryptor encryptor = newEncryptor(recipients, 8, randomFactory); + encryptor.setInput(() -> new ByteArrayInputStream(new byte[] { 1, 2, 3 })); + return encryptor.getStream(); + } + + private static Encryptor newEncryptor(List recipients, int maxRecipients, + IntFunction randomFactory) { + return new Encryptor(recipients, AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128), null, KEY_BYTES, + maxRecipients, 1024, randomFactory); + } + + private static byte[] repeated(byte value) { + byte[] result = new byte[KEY_BYTES]; + Arrays.fill(result, value); + return result; + } + + private static PasswordRecipient passwordRecipient() { + return new PasswordRecipient(new char[] { 'p' }, Pbkdf2Limits.MINIMUM, 16, 32, false, + new Pbkdf2Limits(20_000, 30_000)); + } + + private static void assertAllCleared(List arrays) { + for (byte[] array : arrays) { + assertTrue(Arrays.equals(new byte[array.length], array)); + } + } + + private static class CapturingRecipient implements Recipient { + private final boolean decoy; + private byte[] cek; + + private CapturingRecipient(boolean decoy) { + this.decoy = decoy; + } + + @Override + public boolean decoy() { + return decoy; + } + + @Override + public String id() { + return decoy ? "test-decoy" : "test-real"; + } + + @Override + public byte[] buildRecipientEntry(byte[] inputCek) throws GeneralSecurityException { + cek = inputCek.clone(); + return new byte[] { 1 }; + } + } + + private static final class FailingRecipient extends CapturingRecipient { + private FailingRecipient(boolean decoy) { + super(decoy); + } + + @Override + public byte[] buildRecipientEntry(byte[] inputCek) throws GeneralSecurityException { + throw new GeneralSecurityException("controlled recipient failure"); + } + } + + private static final class RecordingRandomFactory implements IntFunction { + private final List outputs = new ArrayList<>(); + + @Override + public byte[] apply(int length) { + byte[] result = new byte[length]; + Arrays.fill(result, (byte) (outputs.size() + 1)); + outputs.add(result); + return result; + } + + private int calls() { + return outputs.size(); + } + } +} diff --git a/lib/src/test/java/zeroecho/sdk/guard/KemRecipientLifecycleTest.java b/lib/src/test/java/zeroecho/sdk/guard/KemRecipientLifecycleTest.java new file mode 100644 index 0000000..bd3ed76 --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/guard/KemRecipientLifecycleTest.java @@ -0,0 +1,156 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.sdk.guard; + +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.lang.reflect.Field; +import java.io.IOException; +import java.security.Key; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.NullKey; +import zeroecho.core.context.KemContext; +import zeroecho.sdk.Pbkdf2Limits; +import zeroecho.sdk.ZeroEchoSession; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; + +class KemRecipientLifecycleTest { + private static final CryptoAlgorithm ALGORITHM = new TestAlgorithm(); + + @Test + void supportedKekSizesClearSenderSharedSecrets() throws Exception { + System.out.println("supportedKekSizesClearSenderSharedSecrets"); + for (int kekBytes : new int[] { 16, 32 }) { + byte[] senderSecret = filled(32, (byte) 0x41); + ControlledKemContext sender = new ControlledKemContext(senderSecret); + byte[] cek = filled(32, (byte) 0x52); + + byte[] entry = new KemCtxRecipient(sender, kekBytes, 16).buildRecipientEntry(cek); + assertTrue(entry.length > cek.length); + assertArrayEquals(new byte[senderSecret.length], senderSecret); + assertTrue(sender.closed); + System.out.println("...kekBytes=" + kekBytes); + } + System.out.println("...senderSecretsCleared=true"); + System.out.println("supportedKekSizesClearSenderSharedSecrets...ok"); + } + + @Test + void constructorsAndBuilderRejectUnsupportedKekBeforeOwnershipTransfer() throws Exception { + System.out.println("constructorsAndBuilderRejectUnsupportedKekBeforeOwnershipTransfer"); + int[] invalidValues = { -1, 0, 1, 15, 17, 24, 31, 33, Integer.MAX_VALUE }; + ZeroEchoSession session = new ZeroEchoSession() + .withPbkdf2Limits(new Pbkdf2Limits(20_000, 30_000)); + MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session) + .withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader()); + for (int kekBytes : invalidValues) { + ControlledKemContext direct = new ControlledKemContext(filled(32, (byte) 0x11)); + assertThrows(IllegalArgumentException.class, + () -> new KemCtxRecipient(direct, kekBytes, 16)); + assertFalse(direct.closed); + direct.close(); + + ControlledKemContext normal = new ControlledKemContext(filled(32, (byte) 0x22)); + assertThrows(IllegalArgumentException.class, + () -> builder.addRecipient(normal, kekBytes, 16)); + assertEquals(0, recipients(builder).size()); + assertFalse(normal.closed); + normal.close(); + + ControlledKemContext decoy = new ControlledKemContext(filled(32, (byte) 0x33)); + assertThrows(IllegalArgumentException.class, + () -> builder.addRecipientDecoy(decoy, kekBytes, 16)); + assertEquals(0, recipients(builder).size()); + assertFalse(decoy.closed); + decoy.close(); + } + builder.close(); + System.out.println("...recipientCount=0"); + System.out.println("constructorsAndBuilderRejectUnsupportedKekBeforeOwnershipTransfer...ok"); + } + + @Test + void wrapFailureClearsSharedSecretAndClosesContext() { + System.out.println("wrapFailureClearsSharedSecretAndClosesContext"); + byte[] sharedSecret = filled(32, (byte) 0x33); + ControlledKemContext context = new ControlledKemContext(sharedSecret, ALGORITHM, null); + + assertThrows(NullPointerException.class, + () -> new KemCtxRecipient(context, 16, 16).buildRecipientEntry(new byte[32])); + assertArrayEquals(new byte[sharedSecret.length], sharedSecret); + assertTrue(context.closed); + System.out.println("...failureCleanup=true"); + System.out.println("wrapFailureClearsSharedSecretAndClosesContext...ok"); + } + + private static byte[] filled(int length, byte value) { + byte[] result = new byte[length]; + java.util.Arrays.fill(result, value); + return result; + } + + private static final class TestAlgorithm extends CryptoAlgorithm { + private TestAlgorithm() { + super("TEST-KEM", "Test KEM"); + } + } + + private static final class ControlledKemContext implements KemContext { + private final byte[] sharedSecret; + private final CryptoAlgorithm algorithm; + private final byte[] ciphertext; + private boolean closed; + + private ControlledKemContext(byte[] sharedSecret) { + this(sharedSecret, ALGORITHM, new byte[] { 1, 2, 3 }); + } + + private ControlledKemContext(byte[] sharedSecret, CryptoAlgorithm algorithm, byte[] ciphertext) { + this.sharedSecret = sharedSecret; + this.algorithm = algorithm; + this.ciphertext = ciphertext; + } + + @Override + public KemResult encapsulate() { + return new KemResult(ciphertext, sharedSecret); + } + + @Override + public byte[] decapsulate(byte[] ciphertext) { + return sharedSecret; + } + + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + @Override + public Key key() { + return NullKey.INSTANCE; + } + + @Override + public void close() throws IOException { + closed = true; + } + } + + @SuppressWarnings("unchecked") + private static List recipients(MultiRecipientDataSourceBuilder builder) throws Exception { + Field field = MultiRecipientDataSourceBuilder.class.getDeclaredField("recipients"); + field.setAccessible(true); + return (List) field.get(builder); + } +} diff --git a/lib/src/test/java/zeroecho/sdk/guard/MultiRecipientEnvelopeTest.java b/lib/src/test/java/zeroecho/sdk/guard/MultiRecipientEnvelopeTest.java index 7876a6c..74660e3 100644 --- a/lib/src/test/java/zeroecho/sdk/guard/MultiRecipientEnvelopeTest.java +++ b/lib/src/test/java/zeroecho/sdk/guard/MultiRecipientEnvelopeTest.java @@ -34,6 +34,8 @@ package zeroecho.sdk.guard; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -43,6 +45,7 @@ import java.security.KeyPairGenerator; import java.security.SecureRandom; import java.security.Security; import java.security.Signature; +import java.util.Arrays; import java.util.function.Supplier; import java.util.random.RandomGenerator; @@ -62,6 +65,8 @@ import zeroecho.core.alg.sphincsplus.SphincsPlusKeyGenSpec; import zeroecho.core.context.EncryptionContext; import zeroecho.core.context.KemContext; import zeroecho.core.tag.TagEngineBuilder; +import zeroecho.sdk.Pbkdf2Limits; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.sdk.builders.TagTrailerDataContentBuilder; import zeroecho.sdk.builders.alg.AesDataContentBuilder; import zeroecho.sdk.builders.core.DataContentBuilder; @@ -99,10 +104,10 @@ public class MultiRecipientEnvelopeTest { final char[] password = "CorrectHorseBatteryStaple".toCharArray(); // AES-256-GCM with header so IV/tag are persisted in-band - Supplier aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader(); + Supplier aesGcm = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader(); // Encrypt - MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()) .payloadKeyBytes(32) .addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32); @@ -115,8 +120,9 @@ public class MultiRecipientEnvelopeTest { } // Decrypt - MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) - .payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password)); + UnlockMaterial.Password unlockMaterial = new UnlockMaterial.Password(password); + MultiRecipientDataSourceBuilder dec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()) + .payloadKeyBytes(32).unlockWith(unlockMaterial); DataContent decryptor = dec.build(false); decryptor.setInput(new BytesContent(encrypted)); @@ -127,9 +133,54 @@ public class MultiRecipientEnvelopeTest { } assertArrayEquals(input, decrypted); + assertFalse(unlockMaterial.isDestroyed()); + unlockMaterial.destroy(); + assertTrue(unlockMaterial.isDestroyed()); + System.out.println("... borrowed unlock material retained by caller"); System.out.println("...ok"); } + @Test + void passwordRecipientWithAes128KekRoundTrips() throws Exception { + System.out.println("passwordRecipientWithAes128KekRoundTrips"); + byte[] input = randomInput(257); + char[] password = "controlled-password".toCharArray(); + ZeroEchoSession session = new ZeroEchoSession() + .withPbkdf2Limits(new Pbkdf2Limits(1_000_000, 1_000_000)); + byte[] encrypted; + + try (MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session) + .withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader()) + .payloadKeyBytes(32) + .addPasswordRecipient(password, 10_000, 16, 16); + MultiRecipientContent content = builder.build(true)) { + content.setInput(new BytesContent(input)); + try (InputStream stream = content.getStream()) { + encrypted = stream.readAllBytes(); + } + } + + UnlockMaterial.Password unlock = new UnlockMaterial.Password(password); + byte[] decrypted; + try (MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session) + .withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader()) + .payloadKeyBytes(32) + .unlockWith(unlock); + MultiRecipientContent content = builder.build(false)) { + content.setInput(new BytesContent(encrypted)); + try (InputStream stream = content.getStream()) { + decrypted = stream.readAllBytes(); + } + } finally { + unlock.destroy(); + Arrays.fill(password, '\0'); + } + + assertArrayEquals(input, decrypted); + System.out.println("...kekBytes=16"); + System.out.println("passwordRecipientWithAes128KekRoundTrips...ok"); + } + @Test @DisplayName("Password guardian + AES-256-CBC/PKCS7") void testPasswordGuardian_Aes256Cbc() throws Exception { @@ -142,9 +193,9 @@ public class MultiRecipientEnvelopeTest { final char[] password = "Tr0ub4dor&3".toCharArray(); // AES-256-CBC with PKCS7 padding, header persists IV - Supplier aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + Supplier aesCbc = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5().withHeader(); - MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get()) + MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc.get()) .payloadKeyBytes(32) .addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32); @@ -156,7 +207,7 @@ public class MultiRecipientEnvelopeTest { encrypted = readAllBytesAndPrint(es, "... encrypted size"); } - MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get()) + MultiRecipientDataSourceBuilder dec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc.get()) .payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password)); DataContent decryptor = dec.build(false); @@ -184,12 +235,12 @@ public class MultiRecipientEnvelopeTest { final byte[] input = randomInput(128 * 1024 + 7); System.out.println("... input size: " + input.length); - KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); - EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); + KeyPair elg = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + EncryptionContext elgEnc = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); - Supplier aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader(); + Supplier aesGcm = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader(); - MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()) .payloadKeyBytes(32).addRecipient(elgEnc); DataContent encryptor = enc.build(true); @@ -200,7 +251,7 @@ public class MultiRecipientEnvelopeTest { encrypted = readAllBytesAndPrint(es, "... encrypted size"); } - MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + MultiRecipientDataSourceBuilder dec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()) .payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(elg.getPrivate())); DataContent decryptor = dec.build(false); @@ -224,12 +275,12 @@ public class MultiRecipientEnvelopeTest { final byte[] input = randomInput(128 * 1024 + 13); // cross blocks System.out.println("... input size: " + input.length); - KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); - EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); + KeyPair elg = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + EncryptionContext elgEnc = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); - Supplier aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + Supplier aesCbc = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5().withHeader(); - MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get()) + MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc.get()) .payloadKeyBytes(32).addRecipient(elgEnc); DataContent encryptor = enc.build(true); @@ -240,7 +291,7 @@ public class MultiRecipientEnvelopeTest { encrypted = readAllBytesAndPrint(es, "... encrypted size"); } - MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get()) + MultiRecipientDataSourceBuilder dec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc.get()) .payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(elg.getPrivate())); DataContent decryptor = dec.build(false); @@ -272,11 +323,11 @@ public class MultiRecipientEnvelopeTest { kpg.initialize(3072, new SecureRandom()); KeyPair rsa = kpg.generateKeyPair(); - EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); - Supplier aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader(); + Supplier aesGcm = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader(); - MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()) .payloadKeyBytes(32).addRecipient(rsaEnc); DataContent encryptor = enc.build(true); @@ -287,7 +338,7 @@ public class MultiRecipientEnvelopeTest { encrypted = readAllBytesAndPrint(es, "... encrypted size"); } - MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + MultiRecipientDataSourceBuilder dec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()) .payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(rsa.getPrivate())); DataContent decryptor = dec.build(false); @@ -315,11 +366,11 @@ public class MultiRecipientEnvelopeTest { kpg.initialize(3072, new SecureRandom()); KeyPair rsa = kpg.generateKeyPair(); - EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); - Supplier aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + Supplier aesCbc = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5().withHeader(); - MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get()) + MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc.get()) .payloadKeyBytes(32).addRecipient(rsaEnc); DataContent encryptor = enc.build(true); @@ -330,7 +381,7 @@ public class MultiRecipientEnvelopeTest { encrypted = readAllBytesAndPrint(es, "... encrypted size"); } - MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get()) + MultiRecipientDataSourceBuilder dec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc.get()) .payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(rsa.getPrivate())); DataContent decryptor = dec.build(false); @@ -349,6 +400,129 @@ public class MultiRecipientEnvelopeTest { // Multi-recipient (Password + RSA + KEM/ML-KEM + ElGamal) via contexts // ------------------------------------------------------------------------------------ + @Test + void testKemRecipientWith128BitKekRoundTrip() throws Exception { + System.out.println("testKemRecipientWith128BitKekRoundTrip"); + byte[] input = randomInput(257); + KeyPair keyPair = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric() + .generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512()); + + byte[] encrypted = encryptKemRecipients(input, new KeyPair[] { keyPair }, new int[] { 16 }); + decryptAndAssert("...128-bit KEK", aesGcmSupplier(), new UnlockMaterial.Private(keyPair.getPrivate()), + input, encrypted); + + System.out.println("...encryptedLength=" + encrypted.length); + System.out.println("testKemRecipientWith128BitKekRoundTrip...ok"); + } + + @Test + void testMixedKemRecipientKekSizesRoundTrip() throws Exception { + System.out.println("testMixedKemRecipientKekSizesRoundTrip"); + zeroecho.sdk.ZeroEchoSession session = new zeroecho.sdk.ZeroEchoSession(); + KeyPair first = session.keyBuilders().asymmetric() + .generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512()); + KeyPair second = session.keyBuilders().asymmetric() + .generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512()); + byte[] input = randomInput(513); + + byte[] encrypted = encryptKemRecipients(input, new KeyPair[] { first, second }, new int[] { 16, 32 }); + decryptAndAssert("...mixed 16-byte KEK", aesGcmSupplier(), + new UnlockMaterial.Private(first.getPrivate()), input, encrypted); + decryptAndAssert("...mixed 32-byte KEK", aesGcmSupplier(), + new UnlockMaterial.Private(second.getPrivate()), input, encrypted); + + System.out.println("...recipientCount=2"); + System.out.println("testMixedKemRecipientKekSizesRoundTrip...ok"); + } + + @Test + void defaultKemOpenerContinuesAfterSameAlgorithmDecoy() throws Exception { + System.out.println("defaultKemOpenerContinuesAfterSameAlgorithmDecoy"); + ZeroEchoSession session = new ZeroEchoSession(); + KeyPair decoy = session.keyBuilders().asymmetric() + .generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512()); + KeyPair legitimate = session.keyBuilders().asymmetric() + .generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512()); + byte[] input = randomInput(385); + byte[] encrypted; + + KemContext decoyContext = + session.createContext("ML-KEM", KeyUsage.ENCAPSULATE, decoy.getPublic()); + KemContext legitimateContext = + session.createContext("ML-KEM", KeyUsage.ENCAPSULATE, legitimate.getPublic()); + try (MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session) + .withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader()) + .payloadKeyBytes(32) + .addRecipient(decoyContext, 32, 16) + .addRecipient(legitimateContext, 32, 16); + MultiRecipientContent content = builder.build(true)) { + content.setInput(new BytesContent(input)); + try (InputStream stream = content.getStream()) { + encrypted = stream.readAllBytes(); + } + } + + byte[] decrypted; + try (MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session) + .withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader()) + .payloadKeyBytes(32) + .unlockWith(new UnlockMaterial.Private(legitimate.getPrivate())); + MultiRecipientContent content = builder.build(false)) { + content.setInput(new BytesContent(encrypted)); + try (InputStream stream = content.getStream()) { + decrypted = stream.readAllBytes(); + } + } + + assertArrayEquals(input, decrypted); + System.out.println("...sameAlgorithmEntries=2"); + System.out.println("defaultKemOpenerContinuesAfterSameAlgorithmDecoy...ok"); + } + + @Test + void defaultEncryptionOpenerContinuesAfterSameAlgorithmDecoy() throws Exception { + System.out.println("defaultEncryptionOpenerContinuesAfterSameAlgorithmDecoy"); + ZeroEchoSession session = new ZeroEchoSession(); + KeyPair decoy = session.keyBuilders().asymmetric() + .generateKeyPair("RSA", RsaKeyGenSpec.rsa2048()); + KeyPair legitimate = session.keyBuilders().asymmetric() + .generateKeyPair("RSA", RsaKeyGenSpec.rsa2048()); + byte[] input = randomInput(385); + byte[] encrypted; + + EncryptionContext decoyContext = + session.createContext("RSA", KeyUsage.ENCRYPT, decoy.getPublic()); + EncryptionContext legitimateContext = + session.createContext("RSA", KeyUsage.ENCRYPT, legitimate.getPublic()); + try (MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session) + .withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader()) + .payloadKeyBytes(32) + .addRecipient(decoyContext) + .addRecipient(legitimateContext); + MultiRecipientContent content = builder.build(true)) { + content.setInput(new BytesContent(input)); + try (InputStream stream = content.getStream()) { + encrypted = stream.readAllBytes(); + } + } + + byte[] decrypted; + try (MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session) + .withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader()) + .payloadKeyBytes(32) + .unlockWith(new UnlockMaterial.Private(legitimate.getPrivate())); + MultiRecipientContent content = builder.build(false)) { + content.setInput(new BytesContent(encrypted)); + try (InputStream stream = content.getStream()) { + decrypted = stream.readAllBytes(); + } + } + + assertArrayEquals(input, decrypted); + System.out.println("...sameAlgorithmEntries=2"); + System.out.println("defaultEncryptionOpenerContinuesAfterSameAlgorithmDecoy...ok"); + } + @Test @DisplayName("Multi recipients (PWD+RSA+ML-KEM kyber512+ElGamal) + AES-256-GCM") void testMultiRecipients_Kyber512_Aes256Gcm_AllUnlocks() throws Exception { @@ -364,16 +538,16 @@ public class MultiRecipientEnvelopeTest { kpg.initialize(3072, new SecureRandom()); KeyPair rsa = kpg.generateKeyPair(); - KeyPair kyber = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber512()); - KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + KeyPair kyber = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512()); + KeyPair elg = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); - EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); - EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); - KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic()); + EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + EncryptionContext elgEnc = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); + KemContext kybKem = new zeroecho.sdk.ZeroEchoSession().createContext("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic()); - Supplier aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader(); + Supplier aesGcm = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader(); - MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()) .payloadKeyBytes(32) .addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32) .addRecipient(rsaEnc).addRecipient(elgEnc).addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 32); @@ -412,17 +586,17 @@ public class MultiRecipientEnvelopeTest { kpg.initialize(3072, new SecureRandom()); KeyPair rsa = kpg.generateKeyPair(); - rsa = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048()); - KeyPair kyber = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber768()); - KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + rsa = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048()); + KeyPair kyber = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair elg = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); - EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); - EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); - KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic()); + EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + EncryptionContext elgEnc = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); + KemContext kybKem = new zeroecho.sdk.ZeroEchoSession().createContext("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic()); - Supplier aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + Supplier aesCbc = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeCbcPkcs5().withHeader(); - MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get()) + MultiRecipientDataSourceBuilder enc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesCbc.get()) .payloadKeyBytes(32) .addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32) .addRecipient(rsaEnc).addRecipient(elgEnc).addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 32); @@ -463,29 +637,29 @@ public class MultiRecipientEnvelopeTest { KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(3072, new SecureRandom()); KeyPair rsa = kpg.generateKeyPair(); - KeyPair kyber = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber768()); - KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + KeyPair kyber = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair elg = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); // Sender signature keys (Ed25519) - KeyPair ed = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec()); + KeyPair ed = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec()); // AES-256-GCM payload builder - Supplier aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader(); + Supplier aesGcm = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader(); // Context recipients - EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); - EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); - KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic()); + EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + EncryptionContext elgEnc = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); + KemContext kybKem = new zeroecho.sdk.ZeroEchoSession().createContext("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic()); // Envelope (encrypt) - MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + MultiRecipientDataSourceBuilder envEnc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()) .payloadKeyBytes(32).addRecipient(rsaEnc).addRecipient(elgEnc) .addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 16) .addPasswordRecipient(password, /* iterations */ 100_000, /* saltLen */ 16, /* kekBytes */ 32); // Tag trailer for SIGNING (Ed25519) TagTrailerDataContentBuilder signTrailer = new TagTrailerDataContentBuilder<>( - TagEngineBuilder.ed25519Sign(ed.getPrivate())).bufferSize(8192); + TagEngineBuilder.ed25519Sign(new zeroecho.sdk.ZeroEchoSession(), ed.getPrivate())).bufferSize(8192); // Encrypt chain DataContent encryptChain = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)).add(signTrailer) @@ -500,10 +674,10 @@ public class MultiRecipientEnvelopeTest { TagTrailerDataContentBuilder verifyTrailer; // via Password - verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic())) + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(new zeroecho.sdk.ZeroEchoSession(), ed.getPublic())) .bufferSize(8192).throwOnMismatch(); DataContent decPwd = DataContentChainBuilder - .decrypt().add(BytesSourceBuilder.of(ciphertext)).add(new MultiRecipientDataSourceBuilder() + .decrypt().add(BytesSourceBuilder.of(ciphertext)).add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))) .withAes(aesGcm.get()).payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password))) .add(verifyTrailer).build(); byte[] ptPwd; @@ -513,10 +687,10 @@ public class MultiRecipientEnvelopeTest { assertArrayEquals(msg, ptPwd); // via RSA - verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic())) + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(new zeroecho.sdk.ZeroEchoSession(), ed.getPublic())) .bufferSize(8192).throwOnMismatch(); DataContent decRsa = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext)) - .add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32) + .add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()).payloadKeyBytes(32) .unlockWith(new UnlockMaterial.Private(rsa.getPrivate()))) .add(verifyTrailer).build(); byte[] ptRsa; @@ -526,10 +700,10 @@ public class MultiRecipientEnvelopeTest { assertArrayEquals(msg, ptRsa); // via ElGamal - verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic())) + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(new zeroecho.sdk.ZeroEchoSession(), ed.getPublic())) .bufferSize(8192).throwOnMismatch(); DataContent decElgamal = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext)) - .add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32) + .add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()).payloadKeyBytes(32) .unlockWith(new UnlockMaterial.Private(elg.getPrivate()))) .add(verifyTrailer).build(); byte[] ptElgamal; @@ -539,10 +713,10 @@ public class MultiRecipientEnvelopeTest { assertArrayEquals(msg, ptElgamal); // via ML-KEM - verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic())) + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(new zeroecho.sdk.ZeroEchoSession(), ed.getPublic())) .bufferSize(8192).throwOnMismatch(); DataContent decKem = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext)) - .add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32) + .add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()).payloadKeyBytes(32) .unlockWith(new UnlockMaterial.Private(kyber.getPrivate()))) .add(verifyTrailer).build(); byte[] ptKem; @@ -568,28 +742,28 @@ public class MultiRecipientEnvelopeTest { KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(3072, new SecureRandom()); KeyPair rsa = kpg.generateKeyPair(); - KeyPair kyber = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber512()); - KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + KeyPair kyber = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber512()); + KeyPair elg = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); // Sender signature keys (SPHINCS+, default/best) - KeyPair spx = CryptoAlgorithms.keyPair("SPHINCS+", SphincsPlusKeyGenSpec.defaultSpec()); + KeyPair spx = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("SPHINCS+", SphincsPlusKeyGenSpec.defaultSpec()); - Supplier aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader(); + Supplier aesGcm = () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).modeGcm(128).withHeader(); // Context recipients - EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); - EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); - KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic()); + EncryptionContext rsaEnc = new zeroecho.sdk.ZeroEchoSession().createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + EncryptionContext elgEnc = new zeroecho.sdk.ZeroEchoSession().createContext("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); + KemContext kybKem = new zeroecho.sdk.ZeroEchoSession().createContext("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic()); // Envelope recipients - MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + MultiRecipientDataSourceBuilder envEnc = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()) .payloadKeyBytes(32).addRecipient(rsaEnc).addRecipient(elgEnc) .addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 16) .addPasswordRecipient(password, /* iterations */ 120_000, /* saltLen */ 16, /* kekBytes */ 32); // Tag trailer for SIGNING (SPHINCS+) TagTrailerDataContentBuilder signTrailer = new TagTrailerDataContentBuilder<>( - TagEngineBuilder.sphincsPlusSign(spx.getPrivate())).bufferSize(8192); + TagEngineBuilder.sphincsPlusSign(new zeroecho.sdk.ZeroEchoSession(), spx.getPrivate())).bufferSize(8192); // Encrypt chain DataContent encryptChain = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)).add(signTrailer) @@ -603,10 +777,10 @@ public class MultiRecipientEnvelopeTest { TagTrailerDataContentBuilder verifyTrailer; // via Password - verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic())) + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(new zeroecho.sdk.ZeroEchoSession(), spx.getPublic())) .bufferSize(8192).throwOnMismatch(); DataContent decPwd = DataContentChainBuilder - .decrypt().add(BytesSourceBuilder.of(ciphertext)).add(new MultiRecipientDataSourceBuilder() + .decrypt().add(BytesSourceBuilder.of(ciphertext)).add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))) .withAes(aesGcm.get()).payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password))) .add(verifyTrailer).build(); byte[] ptPwd; @@ -616,10 +790,10 @@ public class MultiRecipientEnvelopeTest { assertArrayEquals(msg, ptPwd); // via RSA - verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic())) + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(new zeroecho.sdk.ZeroEchoSession(), spx.getPublic())) .bufferSize(8192).throwOnMismatch(); DataContent decRsa = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext)) - .add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32) + .add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()).payloadKeyBytes(32) .unlockWith(new UnlockMaterial.Private(rsa.getPrivate()))) .add(verifyTrailer).build(); byte[] ptRsa; @@ -629,10 +803,10 @@ public class MultiRecipientEnvelopeTest { assertArrayEquals(msg, ptRsa); // via ElGamal - verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic())) + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(new zeroecho.sdk.ZeroEchoSession(), spx.getPublic())) .bufferSize(8192).throwOnMismatch(); DataContent decElgamal = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext)) - .add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32) + .add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()).payloadKeyBytes(32) .unlockWith(new UnlockMaterial.Private(elg.getPrivate()))) .add(verifyTrailer).build(); byte[] ptElgamal; @@ -642,10 +816,10 @@ public class MultiRecipientEnvelopeTest { assertArrayEquals(msg, ptElgamal); // via ML-KEM - verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic())) + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(new zeroecho.sdk.ZeroEchoSession(), spx.getPublic())) .bufferSize(8192).throwOnMismatch(); DataContent decKem = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext)) - .add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32) + .add(MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesGcm.get()).payloadKeyBytes(32) .unlockWith(new UnlockMaterial.Private(kyber.getPrivate()))) .add(verifyTrailer).build(); byte[] ptKem; @@ -661,6 +835,28 @@ public class MultiRecipientEnvelopeTest { // Helpers // ------------------------------------------------------------------------------------ + private static byte[] encryptKemRecipients(byte[] input, KeyPair[] keyPairs, int[] kekSizes) + throws Exception { + zeroecho.sdk.ZeroEchoSession session = new zeroecho.sdk.ZeroEchoSession(); + MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session) + .withAes(aesGcmSupplier().get()).payloadKeyBytes(32); + for (int index = 0; index < keyPairs.length; index++) { + KemContext context = session.createContext("ML-KEM", KeyUsage.ENCAPSULATE, + keyPairs[index].getPublic()); + builder.addRecipient(context, kekSizes[index], 16); + } + DataContent encryptor = builder.build(true); + encryptor.setInput(new BytesContent(input)); + try (InputStream stream = encryptor.getStream()) { + return stream.readAllBytes(); + } + } + + private static Supplier aesGcmSupplier() { + return () -> AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()) + .modeGcm(128).withHeader(); + } + /** Minimal source builder so we can compose pull-style chains. */ private static final class BytesSourceBuilder implements DataContentBuilder { private final byte[] data; @@ -690,7 +886,7 @@ public class MultiRecipientEnvelopeTest { private static void decryptAndAssert(String banner, Supplier aesFactory, UnlockMaterial material, byte[] original, byte[] encrypted) throws IOException { - MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesFactory.get()) + MultiRecipientDataSourceBuilder dec = MultiRecipientDataSourceBuilder.builder(new zeroecho.sdk.ZeroEchoSession().withPbkdf2Limits(new zeroecho.sdk.Pbkdf2Limits(1_000_000, 1_000_000))).withAes(aesFactory.get()) .payloadKeyBytes(32).unlockWith(material); DataContent decryptor = dec.build(false); diff --git a/lib/src/test/java/zeroecho/sdk/guard/PasswordRecipientTest.java b/lib/src/test/java/zeroecho/sdk/guard/PasswordRecipientTest.java new file mode 100644 index 0000000..0c8b34e --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/guard/PasswordRecipientTest.java @@ -0,0 +1,251 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.sdk.guard; + +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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.io.Util; +import zeroecho.sdk.Pbkdf2Limits; +import zeroecho.sdk.ZeroEchoSession; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; +import zeroecho.sdk.content.api.DataContent; + +class PasswordRecipientTest { + private static final Pbkdf2Limits LIMITS = new Pbkdf2Limits(20_000, 30_000); + + @Test + void ownsAndDestroysRetainedPassword() throws Exception { + System.out.print("PasswordRecipient/lifecycle..."); + char[] caller = "envelope-password".toCharArray(); + char[] expected = caller.clone(); + PasswordRecipient recipient = new PasswordRecipient(caller, 10_000, 16, 32, false, LIMITS); + Arrays.fill(caller, 'x'); + + Field field = PasswordRecipient.class.getDeclaredField("password"); + field.setAccessible(true); + char[] owned = (char[]) field.get(recipient); + assertArrayEquals(expected, owned); + + recipient.destroy(); + recipient.destroy(); + assertTrue(recipient.isDestroyed()); + assertTrue(Arrays.equals(new char[owned.length], owned)); + assertThrows(IllegalStateException.class, () -> recipient.buildRecipientEntry(new byte[32])); + System.out.println("ok"); + } + + @Test + void constructorsRejectIterationsBelowMinimumAndAcceptBoundary() { + System.out.print("PasswordRecipient/constructorsRejectIterationsBelowMinimumAndAcceptBoundary..."); + int[] invalidValues = { -1, 0, 1, 9_999 }; + for (int iterations : invalidValues) { + assertThrows(IllegalArgumentException.class, + () -> new PasswordRecipient(new char[] { 'p' }, iterations, 16, 32, false, LIMITS)); + assertThrows(IllegalArgumentException.class, + () -> new PasswordRecipient(new char[] { 'p' }, iterations, 16, 32, true, LIMITS)); + } + + new PasswordRecipient(new char[] { 'p' }, 10_000, 16, 32, false, LIMITS); + new PasswordRecipient(new char[] { 'p' }, 10_001, 16, 32, true, LIMITS); + System.out.println("...minimum=" + Pbkdf2Limits.MINIMUM); + System.out.println("ok"); + } + + @Test + void constructorsAcceptOnlyOpenableKekSizes() throws Exception { + System.out.println("constructorsAcceptOnlyOpenableKekSizes"); + int[] invalidValues = { -1, 0, 1, 15, 17, 24, 31, 33, Integer.MAX_VALUE }; + for (int kekBytes : invalidValues) { + assertThrows(IllegalArgumentException.class, + () -> new PasswordRecipient(new char[] { 'p' }, 10_000, 16, kekBytes, false, LIMITS)); + } + + PasswordRecipient aes128 = + new PasswordRecipient(new char[] { 'p' }, 10_000, 16, 16, false, LIMITS); + PasswordRecipient aes256 = + new PasswordRecipient(new char[] { 'p' }, 10_000, 16, 32, false, LIMITS); + aes128.close(); + aes256.close(); + System.out.println("...acceptedKekBytes=16,32"); + System.out.println("constructorsAcceptOnlyOpenableKekSizes...ok"); + } + + @Test + void builderMethodsRejectBeforeRecipientListMutation() throws Exception { + System.out.print("PasswordRecipient/builderMethodsRejectBeforeRecipientListMutation..."); + MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder + .builder(new ZeroEchoSession().withPbkdf2Limits(LIMITS)); + int[] invalidValues = { -1, 0, 1, 9_999 }; + + for (int iterations : invalidValues) { + assertThrows(IllegalArgumentException.class, + () -> builder.addPasswordRecipient(new char[] { 'p' }, iterations, 16, 32)); + assertThrows(IllegalArgumentException.class, + () -> builder.addPasswordRecipientDecoy(new char[] { 'p' }, iterations, 16, 32)); + assertEquals(0, recipients(builder).size()); + } + + builder.addPasswordRecipient(new char[] { 'p' }, 10_000, 16, 32); + builder.addPasswordRecipientDecoy(new char[] { 'p' }, 10_001, 16, 32); + assertEquals(2, recipients(builder).size()); + System.out.println("...recipientCount=" + recipients(builder).size()); + System.out.println("ok"); + } + + @Test + void builderRejectsUnsupportedKekBeforeStateMutation() throws Exception { + System.out.println("builderRejectsUnsupportedKekBeforeStateMutation"); + MultiRecipientDataSourceBuilder builder = builderWithAes(); + int[] invalidValues = { -1, 0, 1, 15, 17, 24, 31, 33, Integer.MAX_VALUE }; + + for (int kekBytes : invalidValues) { + assertThrows(IllegalArgumentException.class, + () -> builder.addPasswordRecipient(new char[] { 'p' }, 10_000, 16, kekBytes)); + assertThrows(IllegalArgumentException.class, + () -> builder.addPasswordRecipientDecoy(new char[] { 'p' }, 10_000, 16, kekBytes)); + assertEquals(0, recipients(builder).size()); + } + builder.close(); + System.out.println("...recipientCount=0"); + System.out.println("builderRejectsUnsupportedKekBeforeStateMutation...ok"); + } + + @Test + void passwordOpenerRejectsSubminimumDecodedIterations() throws Exception { + System.out.print("PasswordRecipient/passwordOpenerRejectsSubminimumDecodedIterations..."); + ByteArrayOutputStream blob = new ByteArrayOutputStream(); + Util.writePack7I(blob, 9_999); + + assertThrows(IOException.class, () -> new PasswordOpener(LIMITS).tryOpen( + "PWD:PBKDF2-SHA256:GCM-WRAP", blob.toByteArray(), + new UnlockMaterial.Password(new char[] { 'p' }))); + System.out.println("ok"); + } + + @Test + void passwordOpenerRejectsUnterminatedIterationEncoding() { + System.out.print("PasswordRecipient/passwordOpenerRejectsUnterminatedIterationEncoding..."); + PasswordOpener opener = new PasswordOpener(LIMITS); + UnlockMaterial.Password material = new UnlockMaterial.Password(new char[] { 'p' }); + + assertThrows(IOException.class, + () -> opener.tryOpen("PWD:PBKDF2-SHA256:GCM-WRAP", new byte[] { 1 }, material)); + assertThrows(IOException.class, + () -> opener.tryOpen("PWD:PBKDF2-SHA256:GCM-WRAP", + new byte[] { 1, 1, 1, 1, 1, (byte) 0x80 }, material)); + System.out.println("...malformedCases=2"); + System.out.println("ok"); + } + + @Test + void builderCloseDestroysAbandonedPasswordRecipients() throws Exception { + System.out.println("builderCloseDestroysAbandonedPasswordRecipients"); + MultiRecipientDataSourceBuilder builder = builderWithAes(); + builder.addPasswordRecipient(new char[] { 'p' }, 10_000, 16, 32); + PasswordRecipient recipient = (PasswordRecipient) recipients(builder).get(0); + + builder.close(); + builder.close(); + + assertTrue(builder.isDestroyed()); + assertTrue(recipient.isDestroyed()); + assertThrows(IllegalStateException.class, () -> builder.payloadKeyBytes(16)); + System.out.println("...destroyed=true"); + System.out.println("builderCloseDestroysAbandonedPasswordRecipients...ok"); + } + + @Test + void transferredRecipientSurvivesBuilderCloseUntilTerminalFailure() throws Exception { + System.out.println("transferredRecipientSurvivesBuilderCloseUntilTerminalFailure"); + MultiRecipientDataSourceBuilder builder = builderWithAes(); + builder.addPasswordRecipient(new char[] { 'p' }, 10_000, 16, 32); + PasswordRecipient recipient = (PasswordRecipient) recipients(builder).get(0); + DataContent encryptor = builder.build(true); + + builder.close(); + assertFalse(recipient.isDestroyed()); + assertThrows(NullPointerException.class, encryptor::getStream); + assertTrue(recipient.isDestroyed()); + + System.out.println("...ownershipTransferred=true"); + System.out.println("transferredRecipientSurvivesBuilderCloseUntilTerminalFailure...ok"); + } + + @Test + void abandonedBuiltContentOwnsAndDestroysTransferredRecipient() throws Exception { + System.out.println("abandonedBuiltContentOwnsAndDestroysTransferredRecipient"); + MultiRecipientDataSourceBuilder builder = builderWithAes(); + builder.addPasswordRecipient(new char[] { 'p' }, 10_000, 16, 32); + PasswordRecipient recipient = (PasswordRecipient) recipients(builder).get(0); + MultiRecipientContent content = builder.build(true); + + builder.close(); + assertFalse(recipient.isDestroyed()); + content.close(); + content.close(); + + assertTrue(content.isDestroyed()); + assertTrue(recipient.isDestroyed()); + assertThrows(IllegalStateException.class, + () -> content.setInput(() -> new ByteArrayInputStream(new byte[0]))); + assertThrows(IllegalStateException.class, content::getStream); + System.out.println("...contentDestroyed=true"); + System.out.println("abandonedBuiltContentOwnsAndDestroysTransferredRecipient...ok"); + } + + @Test + void terminalLimitAndRandomFailuresDestroyPasswordRecipients() throws Exception { + System.out.println("terminalLimitAndRandomFailuresDestroyPasswordRecipients"); + MultiRecipientDataSourceBuilder limitedBuilder = builderWithAes().headerLimits(1, 1024); + limitedBuilder.addPasswordRecipient(new char[] { 'a' }, 10_000, 16, 32); + limitedBuilder.addPasswordRecipient(new char[] { 'b' }, 10_000, 16, 32); + List limitedRecipients = List.copyOf(recipients(limitedBuilder)); + DataContent limited = limitedBuilder.build(true); + limited.setInput(() -> new ByteArrayInputStream(new byte[0])); + assertThrows(IOException.class, limited::getStream); + assertTrue(limitedRecipients.stream() + .map(PasswordRecipient.class::cast).allMatch(PasswordRecipient::isDestroyed)); + + PasswordRecipient randomRecipient = + new PasswordRecipient(new char[] { 'c' }, 10_000, 16, 32, false, LIMITS); + Encryptor randomFailure = new Encryptor(List.of(randomRecipient), + AesDataContentBuilder.builder(new ZeroEchoSession()).modeGcm(128).withHeader(), + null, 32, 4, 1024, ignored -> { + throw new IllegalStateException("controlled random failure"); + }); + randomFailure.setInput(() -> new ByteArrayInputStream(new byte[0])); + assertThrows(IllegalStateException.class, randomFailure::getStream); + assertTrue(randomRecipient.isDestroyed()); + + System.out.println("...terminalFailures=2"); + System.out.println("terminalLimitAndRandomFailuresDestroyPasswordRecipients...ok"); + } + + private static MultiRecipientDataSourceBuilder builderWithAes() { + ZeroEchoSession session = new ZeroEchoSession().withPbkdf2Limits(LIMITS); + return MultiRecipientDataSourceBuilder.builder(session) + .withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader()); + } + + @SuppressWarnings("unchecked") + private static List recipients(MultiRecipientDataSourceBuilder builder) throws Exception { + Field field = MultiRecipientDataSourceBuilder.class.getDeclaredField("recipients"); + field.setAccessible(true); + return (List) field.get(builder); + } +} diff --git a/lib/src/test/java/zeroecho/sdk/guard/SessionRecipientOpenerContractTest.java b/lib/src/test/java/zeroecho/sdk/guard/SessionRecipientOpenerContractTest.java new file mode 100644 index 0000000..86250a7 --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/guard/SessionRecipientOpenerContractTest.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.sdk.guard; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.io.Util; +import zeroecho.sdk.Pbkdf2Limits; +import zeroecho.sdk.ZeroEchoSession; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; + +class SessionRecipientOpenerContractTest { + + @Test + void publicApiExposesOnlyReusableSessionOpeners() { + System.out.println("publicApiExposesOnlyReusableSessionOpeners"); + assertSessionOnlyConstructor(KemCtxOpener.class); + assertSessionOnlyConstructor(EncCtxOpener.class); + + List addOpenerMethods = Arrays.stream(MultiRecipientDataSourceBuilder.class.getMethods()) + .filter(method -> method.getName().equals("addOpener")) + .toList(); + assertEquals(1, addOpenerMethods.size()); + assertEquals(RecipientOpener.class, addOpenerMethods.get(0).getParameterTypes()[0]); + + System.out.println("...addOpenerOverloads=1"); + System.out.println("publicApiExposesOnlyReusableSessionOpeners...ok"); + } + + @Test + void sessionOpenersIgnoreUnrelatedEntryFamilies() throws Exception { + System.out.println("sessionOpenersIgnoreUnrelatedEntryFamilies"); + ZeroEchoSession session = new ZeroEchoSession(); + UnlockMaterial.Password material = new UnlockMaterial.Password(new char[] { 'p' }); + try { + assertNull(new KemCtxOpener(session).tryOpen( + "CTX-ENC:RSA", new byte[0], material)); + assertNull(new EncCtxOpener(session).tryOpen( + "KEM:ML-KEM:GCM-WRAP", new byte[0], material)); + } finally { + material.destroy(); + } + System.out.println("...ignoredFamilies=2"); + System.out.println("sessionOpenersIgnoreUnrelatedEntryFamilies...ok"); + } + + @Test + void customReusableOpenerScansEveryEntryAndClosesOnce() throws Exception { + System.out.println("customReusableOpenerScansEveryEntryAndClosesOnce"); + ZeroEchoSession session = new ZeroEchoSession() + .withPbkdf2Limits(new Pbkdf2Limits(20_000, 30_000)); + TrackingOpener opener = new TrackingOpener(); + UnlockMaterial.Password material = new UnlockMaterial.Password(new char[] { 'p' }); + try (MultiRecipientDataSourceBuilder builder = MultiRecipientDataSourceBuilder.builder(session) + .withAes(AesDataContentBuilder.builder(session).modeGcm(128).withHeader()) + .unlockWith(material) + .addOpener(opener); + MultiRecipientContent content = builder.build(false)) { + content.setInput(() -> new ByteArrayInputStream(twoEntryHeader())); + assertThrows(IOException.class, content::getStream); + } finally { + material.destroy(); + } + + assertEquals(2, opener.attempts); + assertEquals(1, opener.closeCount); + System.out.println("...attempts=" + opener.attempts); + System.out.println("customReusableOpenerScansEveryEntryAndClosesOnce...ok"); + } + + private static void assertSessionOnlyConstructor(Class openerClass) { + Constructor[] constructors = openerClass.getConstructors(); + assertEquals(1, constructors.length); + assertEquals(ZeroEchoSession.class, constructors[0].getParameterTypes()[0]); + } + + private static byte[] twoEntryHeader() throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + Util.writePack7I(output, 2); + Util.writeUTF8(output, "first"); + Util.write(output, new byte[] { 1 }); + Util.writeUTF8(output, "second"); + Util.write(output, new byte[] { 2 }); + return output.toByteArray(); + } + + private static final class TrackingOpener implements RecipientOpener { + private int attempts; + private int closeCount; + + @Override + public byte[] tryOpen(String entryId, byte[] entryBlob, UnlockMaterial material) { + attempts++; + return null; + } + + @Override + public void close() { + closeCount++; + } + } +} diff --git a/lib/src/test/java/zeroecho/sdk/hybrid/derived/HybridDerivedTest.java b/lib/src/test/java/zeroecho/sdk/hybrid/derived/HybridDerivedTest.java index bb40982..93539b4 100644 --- a/lib/src/test/java/zeroecho/sdk/hybrid/derived/HybridDerivedTest.java +++ b/lib/src/test/java/zeroecho/sdk/hybrid/derived/HybridDerivedTest.java @@ -76,7 +76,7 @@ public class HybridDerivedTest { byte[] aad = "aad".getBytes(StandardCharsets.UTF_8); byte[] msg = fixedBytes(1024, (byte) 0x5A); - AesDataContentBuilder encAes = AesDataContentBuilder.builder().withHeader().modeGcm(128); + AesDataContentBuilder encAes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withHeader().modeGcm(128); AesDataContentBuilder returnedEnc = HybridDerived.from(exporter).label("app/enc/aes").transcript(transcript) .aad(aad).applyToAesGcm(encAes, 256, 12); @@ -88,7 +88,7 @@ public class HybridDerivedTest { System.out.println("...ciphertextLen=" + ciphertext.length); System.out.println("...ciphertextPrefix=" + shortHex(ciphertext, 32)); - AesDataContentBuilder decAes = AesDataContentBuilder.builder().withHeader().modeGcm(128); + AesDataContentBuilder decAes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withHeader().modeGcm(128); HybridDerived.from(exporter).label("app/enc/aes").transcript(transcript).aad(aad).applyToAesGcm(decAes, 256, 12); @@ -109,7 +109,7 @@ public class HybridDerivedTest { byte[] aad = "aad".getBytes(StandardCharsets.UTF_8); byte[] msg = fixedBytes(256, (byte) 0x1C); - AesDataContentBuilder encAes = AesDataContentBuilder.builder().withHeader().modeGcm(128); + AesDataContentBuilder encAes = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withHeader().modeGcm(128); HybridDerived.from(exporter).label("app/enc/aes").transcript(transcript).aad(aad).applyToAesGcm(encAes, 256, 12); @@ -117,7 +117,7 @@ public class HybridDerivedTest { byte[] ciphertext = runEncrypt(encAes, msg); System.out.println("...ciphertextLen=" + ciphertext.length); - AesDataContentBuilder decAesWrong = AesDataContentBuilder.builder().withHeader().modeGcm(128); + AesDataContentBuilder decAesWrong = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withHeader().modeGcm(128); // ...label mismatch -> wrong key/iv/aad -> decryption must fail HybridDerived.from(exporter).label("app/enc/aes_WRONG").transcript(transcript).aad(aad) @@ -137,7 +137,7 @@ public class HybridDerivedTest { byte[] aad = "aad".getBytes(StandardCharsets.UTF_8); byte[] msg = fixedBytes(777, (byte) 0x33); - ChaChaDataContentBuilder encChaCha = ChaChaDataContentBuilder.builder().withHeader(); + ChaChaDataContentBuilder encChaCha = ChaChaDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withHeader(); ChaChaDataContentBuilder returnedEnc = HybridDerived.from(exporter).label("app/enc/chacha") .transcript(transcript).aad(aad).applyToChaChaAead(encChaCha, 256, 12); @@ -149,7 +149,7 @@ public class HybridDerivedTest { System.out.println("...ciphertextLen=" + ciphertext.length); System.out.println("...ciphertextPrefix=" + shortHex(ciphertext, 32)); - ChaChaDataContentBuilder decChaCha = ChaChaDataContentBuilder.builder().withHeader(); + ChaChaDataContentBuilder decChaCha = ChaChaDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).withHeader(); HybridDerived.from(exporter).label("app/enc/chacha").transcript(transcript).aad(aad) .applyToChaChaAead(decChaCha, 256, 12); @@ -174,7 +174,7 @@ public class HybridDerivedTest { // recommended bits // -------------------- - HmacDataContentBuilder macBuilder = HmacDataContentBuilder.builder().sha256().emitHexTag(); + HmacDataContentBuilder macBuilder = HmacDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).sha256().emitHexTag(); int recommendedBits = macBuilder.recommendedKeyBits(); System.out.println("...recommendedBits=" + recommendedBits); @@ -184,7 +184,7 @@ public class HybridDerivedTest { String tagHex = runHmacHex(macBuilder, msg); System.out.println("...tagHexPrefix=" + shortText(tagHex, 64)); - HmacDataContentBuilder verifyBuilder = HmacDataContentBuilder.builder().sha256().expectedTagHex(tagHex) + HmacDataContentBuilder verifyBuilder = HmacDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).sha256().expectedTagHex(tagHex) .emitVerificationBoolean(); HybridDerived.from(exporter).label("app/mac/hmac-default").transcript(transcript).applyToHmac(verifyBuilder); @@ -197,7 +197,7 @@ public class HybridDerivedTest { // Override key size path: applyToHmac(hmac, keyBits) // -------------------- - HmacDataContentBuilder macBuilderOv = HmacDataContentBuilder.builder().sha256().emitHexTag(); + HmacDataContentBuilder macBuilderOv = HmacDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).sha256().emitHexTag(); // ...override to 512-bit keying material (still valid for HMAC; explicit expert // choice) @@ -207,7 +207,7 @@ public class HybridDerivedTest { String tagHexOv = runHmacHex(macBuilderOv, msg); System.out.println("...tagHexOvPrefix=" + shortText(tagHexOv, 64)); - HmacDataContentBuilder verifyBuilderOv = HmacDataContentBuilder.builder().sha256().expectedTagHex(tagHexOv) + HmacDataContentBuilder verifyBuilderOv = HmacDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).sha256().expectedTagHex(tagHexOv) .emitVerificationBoolean(); HybridDerived.from(exporter).label("app/mac/hmac-override").transcript(transcript).applyToHmac(verifyBuilderOv, @@ -221,7 +221,7 @@ public class HybridDerivedTest { // Negative: wrong expected tag -> must emit "false" // -------------------- - HmacDataContentBuilder verifyBad = HmacDataContentBuilder.builder().sha256() + HmacDataContentBuilder verifyBad = HmacDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).sha256() .expectedTagHex(tagHex.substring(0, Math.max(0, tagHex.length() - 2)) + "00").emitVerificationBoolean(); HybridDerived.from(exporter).label("app/mac/hmac-default").transcript(transcript).applyToHmac(verifyBad); diff --git a/lib/src/test/java/zeroecho/sdk/hybrid/kex/HybridKexExporterLifecycleTest.java b/lib/src/test/java/zeroecho/sdk/hybrid/kex/HybridKexExporterLifecycleTest.java new file mode 100644 index 0000000..cffedfa --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/hybrid/kex/HybridKexExporterLifecycleTest.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.sdk.hybrid.kex; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Field; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +class HybridKexExporterLifecycleTest { + + @Test + void constructorOwnsDefensiveCopies() throws Exception { + System.out.println("constructorOwnsDefensiveCopies"); + byte[] root = filled(32, (byte) 1); + byte[] salt = filled(16, (byte) 2); + HybridKexExporter exporter = new HybridKexExporter(root, salt); + Arrays.fill(root, (byte) 0); + Arrays.fill(salt, (byte) 0); + + assertFalse(Arrays.equals(root, exporter.rootSecretCopy())); + assertArrayEquals(filled(32, (byte) 1), internal(exporter, "rootSecret")); + assertArrayEquals(filled(16, (byte) 2), internal(exporter, "salt")); + exporter.close(); + System.out.println("...ownedCopies=true"); + System.out.println("constructorOwnsDefensiveCopies...ok"); + } + + @Test + void destructionClearsSecretsAndRejectsFurtherUse() throws Exception { + System.out.println("destructionClearsSecretsAndRejectsFurtherUse"); + HybridKexExporter exporter = new HybridKexExporter(filled(32, (byte) 3), filled(16, (byte) 4)); + byte[] internalRoot = internal(exporter, "rootSecret"); + byte[] internalSalt = internal(exporter, "salt"); + + exporter.destroy(); + exporter.destroy(); + exporter.close(); + + assertTrue(exporter.isDestroyed()); + assertArrayEquals(new byte[internalRoot.length], internalRoot); + assertArrayEquals(new byte[internalSalt.length], internalSalt); + assertThrows(IllegalStateException.class, () -> exporter.export("label", null, 16)); + assertThrows(IllegalStateException.class, exporter::rootSecretCopy); + System.out.println("...destroyed=true...postDestroyRejected=true"); + System.out.println("destructionClearsSecretsAndRejectsFurtherUse...ok"); + } + + @Test + void successfulExportDoesNotDestroyExporter() { + System.out.println("successfulExportDoesNotDestroyExporter"); + HybridKexExporter exporter = new HybridKexExporter(filled(32, (byte) 5), null); + + byte[] first = exporter.export("first", null, 16); + byte[] second = exporter.export("second", new byte[] { 1 }, 16); + + assertFalse(exporter.isDestroyed()); + assertFalse(Arrays.equals(first, second)); + exporter.close(); + Arrays.fill(first, (byte) 0); + Arrays.fill(second, (byte) 0); + System.out.println("...derivedLengths=16,16"); + System.out.println("successfulExportDoesNotDestroyExporter...ok"); + } + + private static byte[] internal(HybridKexExporter exporter, String fieldName) throws Exception { + Field field = HybridKexExporter.class.getDeclaredField(fieldName); + field.setAccessible(true); + return (byte[]) field.get(exporter); + } + + private static byte[] filled(int length, byte value) { + byte[] result = new byte[length]; + Arrays.fill(result, value); + return result; + } +} diff --git a/lib/src/test/java/zeroecho/sdk/hybrid/kex/HybridKexFrameCodecTest.java b/lib/src/test/java/zeroecho/sdk/hybrid/kex/HybridKexFrameCodecTest.java new file mode 100644 index 0000000..7260380 --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/hybrid/kex/HybridKexFrameCodecTest.java @@ -0,0 +1,196 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.sdk.hybrid.kex; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.context.AgreementContext; +import zeroecho.core.context.MessageAgreementContext; + +/** + * Verifies provider-independent hybrid frame validation and size boundaries. + */ +class HybridKexFrameCodecTest { + + @Test + void acceptsZeroLengthComponents() throws Exception { + String name = start("acceptsZeroLengthComponents"); + byte[] frame = HybridKexContext.encode(new byte[0], new byte[0]); + HybridKexContext.Parts parts = HybridKexContext.decode(frame); + + assertEquals(8, frame.length); + assertArrayEquals(new byte[0], parts.classicPart()); + assertArrayEquals(new byte[0], parts.pqcPart()); + progress("frameBytes=" + frame.length); + ok(name); + } + + @Test + void acceptsInclusiveMaximumFrame() throws Exception { + String name = start("acceptsInclusiveMaximumFrame"); + byte[] classic = new byte[HybridKexContext.MAX_FRAME_BYTES - 8]; + byte[] frame = HybridKexContext.encode(classic, new byte[0]); + + assertEquals(HybridKexContext.MAX_FRAME_BYTES, frame.length); + assertEquals(classic.length, HybridKexContext.decode(frame).classicPart().length); + progress("frameBytes=" + frame.length); + ok(name); + } + + @Test + void rejectsOversizedEncoding() { + String name = start("rejectsOversizedEncoding"); + byte[] classic = new byte[HybridKexContext.MAX_FRAME_BYTES - 7]; + + assertThrows(IOException.class, () -> HybridKexContext.encode(classic, new byte[0])); + progress("classicBytes=" + classic.length); + ok(name); + } + + @Test + void rejectsMalformedFrames() { + String name = start("rejectsMalformedFrames"); + byte[][] malformed = { + new byte[7], + ints(-1, 0), + ints(0, -1), + ints(1, 0), + ints(0, 1), + append(ints(0, 0), (byte) 0x7f), + new byte[HybridKexContext.MAX_FRAME_BYTES + 1], + ints(0, HybridKexContext.MAX_FRAME_BYTES), + ints(Integer.MAX_VALUE, 0) + }; + + for (byte[] frame : malformed) { + assertThrows(IOException.class, () -> HybridKexContext.decode(frame)); + } + progress("cases=" + malformed.length); + ok(name); + } + + @Test + void publicApiMapsMalformedFrame() { + String name = start("publicApiMapsMalformedFrame"); + AgreementContext classic = mock(AgreementContext.class); + MessageAgreementContext pqc = mock(MessageAgreementContext.class); + HybridKexContext context = new HybridKexContext(HybridKexProfile.defaultProfile(32), classic, pqc); + + IllegalArgumentException failure = assertThrows(IllegalArgumentException.class, + () -> context.setPeerMessage(ints(Integer.MAX_VALUE, 0))); + + assertInstanceOf(IOException.class, failure.getCause()); + verifyNoInteractions(classic, pqc); + progress("mapped=IllegalArgument"); + ok(name); + } + + @Test + void clearsComponentSecretsAfterSuccessfulDerivation() { + String name = start("clearsComponentSecretsAfterSuccessfulDerivation"); + byte[] classicSecret = { 1, 2, 3 }; + byte[] pqcSecret = { 4, 5, 6 }; + AgreementContext classic = mock(AgreementContext.class); + MessageAgreementContext pqc = mock(MessageAgreementContext.class); + when(classic.deriveSecret()).thenReturn(classicSecret); + when(pqc.deriveSecret()).thenReturn(pqcSecret); + HybridKexContext context = new HybridKexContext(HybridKexProfile.defaultProfile(16), classic, pqc); + + byte[] result = context.deriveSecret(); + + assertEquals(16, result.length); + assertZeroized(classicSecret); + assertZeroized(pqcSecret); + progress("resultBytes=" + result.length); + ok(name); + } + + @Test + void clearsClassicSecretWhenSecondLegFails() { + String name = start("clearsClassicSecretWhenSecondLegFails"); + byte[] classicSecret = { 1, 2, 3 }; + IllegalStateException expected = new IllegalStateException("controlled second-leg failure"); + AgreementContext classic = mock(AgreementContext.class); + MessageAgreementContext pqc = mock(MessageAgreementContext.class); + when(classic.deriveSecret()).thenReturn(classicSecret); + when(pqc.deriveSecret()).thenThrow(expected); + HybridKexContext context = new HybridKexContext(HybridKexProfile.defaultProfile(16), classic, pqc); + + assertSame(expected, assertThrows(IllegalStateException.class, context::deriveSecret)); + assertZeroized(classicSecret); + progress("failure=secondLeg"); + ok(name); + } + + @Test + void clearsAllTemporarySecretsWhenKdfFails() { + String name = start("clearsAllTemporarySecretsWhenKdfFails"); + byte[] classicSecret = { 1, 2, 3 }; + byte[] pqcSecret = { 4, 5, 6 }; + byte[][] observedInput = new byte[1][]; + AgreementContext classic = mock(AgreementContext.class); + MessageAgreementContext pqc = mock(MessageAgreementContext.class); + when(classic.deriveSecret()).thenReturn(classicSecret); + when(pqc.deriveSecret()).thenReturn(pqcSecret); + HybridKexContext context = new HybridKexContext(HybridKexProfile.defaultProfile(16), classic, pqc, + (ikm, salt, info, outputLength) -> { + observedInput[0] = ikm; + throw new GeneralSecurityException("controlled KDF failure"); + }); + + IllegalStateException failure = assertThrows(IllegalStateException.class, context::deriveSecret); + + assertInstanceOf(GeneralSecurityException.class, failure.getCause()); + assertZeroized(classicSecret); + assertZeroized(pqcSecret); + assertZeroized(observedInput[0]); + progress("failure=KDF"); + ok(name); + } + + private static byte[] ints(int first, int second) { + return ByteBuffer.allocate(8).putInt(first).putInt(second).array(); + } + + private static byte[] append(byte[] input, byte value) { + byte[] output = new byte[input.length + 1]; + System.arraycopy(input, 0, output, 0, input.length); + output[input.length] = value; + return output; + } + + private static void assertZeroized(byte[] value) { + assertTrue(value != null && Arrays.equals(new byte[value.length], value)); + } + + private static String start(String routine) { + String label = routine.length() <= 30 ? routine : routine.substring(0, 27) + "..."; + System.out.println(label); + return label; + } + + private static void progress(String detail) { + System.out.println("..." + detail); + } + + private static void ok(String name) { + System.out.println(name + "...ok"); + } +} diff --git a/lib/src/test/java/zeroecho/sdk/hybrid/kex/HybridKexTest.java b/lib/src/test/java/zeroecho/sdk/hybrid/kex/HybridKexTest.java index b9cfe5b..774e16c 100644 --- a/lib/src/test/java/zeroecho/sdk/hybrid/kex/HybridKexTest.java +++ b/lib/src/test/java/zeroecho/sdk/hybrid/kex/HybridKexTest.java @@ -129,11 +129,11 @@ public class HybridKexTest { HybridKexProfile profile = HybridKexProfile.defaultProfile(32); // Classic: X25519 key pairs (Xdh + XdhSpec.X25519) - KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); // PQC: ML-KEM key pair (Kyber variant) - KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); HybridKexContext alice = null; HybridKexContext bob = null; @@ -141,12 +141,12 @@ public class HybridKexTest { try { // Initiator: classic uses Alice private + Bob classic public; PQC uses Bob PQC // public - alice = HybridKexContexts.initiator(profile, "Xdh", aliceClassic.getPrivate(), bobClassic.getPublic(), + alice = HybridKexContexts.initiator(new zeroecho.sdk.ZeroEchoSession(), profile, "Xdh", aliceClassic.getPrivate(), bobClassic.getPublic(), XdhSpec.X25519, "ML-KEM", bobPqc.getPublic(), null); // Responder: classic uses Bob private + Alice classic public; PQC uses Bob PQC // private - bob = HybridKexContexts.responder(profile, "Xdh", bobClassic.getPrivate(), aliceClassic.getPublic(), + bob = HybridKexContexts.responder(new zeroecho.sdk.ZeroEchoSession(), profile, "Xdh", bobClassic.getPrivate(), aliceClassic.getPublic(), XdhSpec.X25519, "ML-KEM", bobPqc.getPrivate(), null); // Alice produces message (contains PQC ciphertext; classic part is empty here) @@ -190,11 +190,11 @@ public class HybridKexTest { HybridKexProfile profile = HybridKexProfile.defaultProfile(32); // Classic: X25519 key pairs (Xdh + XdhSpec.X25519) - KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair aliceClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); // PQC: ML-KEM key pair (recipient/responder) - KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair bobPqc = new zeroecho.sdk.ZeroEchoSession().keyBuilders().asymmetric().generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); HybridKexContext alice = null; HybridKexContext bob = null; @@ -204,10 +204,10 @@ public class HybridKexTest { // KeyPairKey + ContextSpec). // PQC leg is KEM-style: initiator uses recipient public key; responder uses // recipient private key. - alice = HybridKexContexts.initiatorPairMessage(profile, "Xdh", new KeyPairKey(aliceClassic), XdhSpec.X25519, + alice = HybridKexContexts.initiatorPairMessage(new zeroecho.sdk.ZeroEchoSession(), profile, "Xdh", new KeyPairKey(aliceClassic), XdhSpec.X25519, "ML-KEM", bobPqc.getPublic(), null); - bob = HybridKexContexts.responderPairMessage(profile, "Xdh", new KeyPairKey(bobClassic), XdhSpec.X25519, + bob = HybridKexContexts.responderPairMessage(new zeroecho.sdk.ZeroEchoSession(), profile, "Xdh", new KeyPairKey(bobClassic), XdhSpec.X25519, "ML-KEM", bobPqc.getPrivate(), null); // Step 1: Alice -> Bob (classic SPKI + PQC ciphertext) diff --git a/lib/src/test/java/zeroecho/sdk/hybrid/signature/HybridSignatureTest.java b/lib/src/test/java/zeroecho/sdk/hybrid/signature/HybridSignatureTest.java index 2d790dd..9817fbc 100644 --- a/lib/src/test/java/zeroecho/sdk/hybrid/signature/HybridSignatureTest.java +++ b/lib/src/test/java/zeroecho/sdk/hybrid/signature/HybridSignatureTest.java @@ -53,10 +53,14 @@ import org.junit.jupiter.api.Test; import zeroecho.core.CryptoAlgorithms; import zeroecho.core.KeyUsage; +import zeroecho.core.alg.ed25519.Ed25519KeyGenSpec; +import zeroecho.core.alg.rsa.RsaKeyGenSpec; +import zeroecho.core.alg.sphincsplus.SphincsPlusKeyGenSpec; import zeroecho.core.context.SignatureContext; import zeroecho.core.io.TailStrippingInputStream; import zeroecho.core.spec.ContextSpec; import zeroecho.sdk.builders.TagTrailerDataContentBuilder; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.sdk.builders.core.DataContentBuilder; import zeroecho.sdk.builders.core.DataContentChainBuilder; import zeroecho.sdk.content.api.DataContent; @@ -167,7 +171,7 @@ public class HybridSignatureTest { } private static int tagLen(String algoId, KeyUsage role, Key key, ContextSpec specOrNull) throws Exception { - try (SignatureContext ctx = CryptoAlgorithms.create(algoId, role, key, specOrNull)) { + try (SignatureContext ctx = new zeroecho.sdk.ZeroEchoSession().createContext(algoId, role, key, specOrNull)) { return ctx.tagLength(); } } @@ -237,8 +241,11 @@ public class HybridSignatureTest { byte[] msg = randomBytes(size); System.out.println("...msg=" + msg.length + " bytes"); - KeyPair ed = CryptoAlgorithms.require("Ed25519").generateKeyPair(); - KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair(); + ZeroEchoSession session = new ZeroEchoSession(); + KeyPair ed = session.keyBuilders().asymmetric().generateKeyPair("Ed25519", + Ed25519KeyGenSpec.defaultSpec()); + KeyPair spx = session.keyBuilders().asymmetric().generateKeyPair("SPHINCS+", + SphincsPlusKeyGenSpec.defaultSpec()); int edLen = tagLen("Ed25519", KeyUsage.SIGN, ed.getPrivate(), null); int spxLen = tagLen("SPHINCS+", KeyUsage.SIGN, spx.getPrivate(), null); @@ -249,14 +256,14 @@ public class HybridSignatureTest { HybridSignatureProfile.VerifyRule.AND); byte[] sigAnd; - try (SignatureContext signer = HybridSignatureContexts.sign(andProfile, ed.getPrivate(), spx.getPrivate(), + try (SignatureContext signer = HybridSignatureContexts.sign(new zeroecho.sdk.ZeroEchoSession(), andProfile, ed.getPrivate(), spx.getPrivate(), 2 * 1024 * 1024)) { sigAnd = signTrailer(signer, msg); } System.out.println("...sig(AND).len=" + sigAnd.length + ", head=" + hexShort(sigAnd)); // verify OK - try (SignatureContext verifier = HybridSignatureContexts.verify(andProfile, ed.getPublic(), spx.getPublic(), + try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), andProfile, ed.getPublic(), spx.getPublic(), 2 * 1024 * 1024)) { verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch()); verifier.setExpectedTag(sigAnd); @@ -268,7 +275,7 @@ public class HybridSignatureTest { // corrupt classic => must fail byte[] badClassic = concat(flipOneBit(sub(sigAnd, 0, edLen), 0), sub(sigAnd, edLen, spxLen)); - try (SignatureContext verifier = HybridSignatureContexts.verify(andProfile, ed.getPublic(), spx.getPublic(), + try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), andProfile, ed.getPublic(), spx.getPublic(), 2 * 1024 * 1024)) { verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch()); verifier.setExpectedTag(badClassic); @@ -282,7 +289,7 @@ public class HybridSignatureTest { // corrupt pqc => must fail byte[] badPqc = concat(sub(sigAnd, 0, edLen), flipOneBit(sub(sigAnd, edLen, spxLen), 0)); - try (SignatureContext verifier = HybridSignatureContexts.verify(andProfile, ed.getPublic(), spx.getPublic(), + try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), andProfile, ed.getPublic(), spx.getPublic(), 2 * 1024 * 1024)) { verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch()); verifier.setExpectedTag(badPqc); @@ -299,7 +306,7 @@ public class HybridSignatureTest { HybridSignatureProfile.VerifyRule.OR); byte[] sigOr; - try (SignatureContext signer = HybridSignatureContexts.sign(orProfile, ed.getPrivate(), spx.getPrivate(), + try (SignatureContext signer = HybridSignatureContexts.sign(new zeroecho.sdk.ZeroEchoSession(), orProfile, ed.getPrivate(), spx.getPrivate(), 2 * 1024 * 1024)) { sigOr = signTrailer(signer, msg); } @@ -307,7 +314,7 @@ public class HybridSignatureTest { // corrupt classic => OR must pass byte[] orBadClassic = concat(flipOneBit(sub(sigOr, 0, edLen), 0), sub(sigOr, edLen, spxLen)); - try (SignatureContext verifier = HybridSignatureContexts.verify(orProfile, ed.getPublic(), spx.getPublic(), + try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), orProfile, ed.getPublic(), spx.getPublic(), 2 * 1024 * 1024)) { verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch()); verifier.setExpectedTag(orBadClassic); @@ -319,7 +326,7 @@ public class HybridSignatureTest { // corrupt pqc => OR must pass byte[] orBadPqc = concat(sub(sigOr, 0, edLen), flipOneBit(sub(sigOr, edLen, spxLen), 0)); - try (SignatureContext verifier = HybridSignatureContexts.verify(orProfile, ed.getPublic(), spx.getPublic(), + try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), orProfile, ed.getPublic(), spx.getPublic(), 2 * 1024 * 1024)) { verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch()); verifier.setExpectedTag(orBadPqc); @@ -331,7 +338,7 @@ public class HybridSignatureTest { // corrupt both => OR must fail byte[] orBadBoth = concat(flipOneBit(sub(sigOr, 0, edLen), 0), flipOneBit(sub(sigOr, edLen, spxLen), 0)); - try (SignatureContext verifier = HybridSignatureContexts.verify(orProfile, ed.getPublic(), spx.getPublic(), + try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), orProfile, ed.getPublic(), spx.getPublic(), 2 * 1024 * 1024)) { verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch()); verifier.setExpectedTag(orBadBoth); @@ -357,8 +364,10 @@ public class HybridSignatureTest { byte[] msg = randomBytes(size); System.out.println("...msg=" + msg.length + " bytes"); - KeyPair rsa = CryptoAlgorithms.require("RSA").generateKeyPair(); - KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair(); + ZeroEchoSession session = new ZeroEchoSession(); + KeyPair rsa = session.keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048()); + KeyPair spx = session.keyBuilders().asymmetric().generateKeyPair("SPHINCS+", + SphincsPlusKeyGenSpec.defaultSpec()); int rsaLen = tagLen("RSA", KeyUsage.SIGN, rsa.getPrivate(), null); int spxLen = tagLen("SPHINCS+", KeyUsage.SIGN, spx.getPrivate(), null); @@ -368,13 +377,13 @@ public class HybridSignatureTest { HybridSignatureProfile.VerifyRule.AND); byte[] sig; - try (SignatureContext signer = HybridSignatureContexts.sign(profile, rsa.getPrivate(), spx.getPrivate(), + try (SignatureContext signer = HybridSignatureContexts.sign(new zeroecho.sdk.ZeroEchoSession(), profile, rsa.getPrivate(), spx.getPrivate(), 2 * 1024 * 1024)) { sig = signTrailer(signer, msg); } System.out.println("...sig.len=" + sig.length + ", head=" + hexShort(sig)); - try (SignatureContext verifier = HybridSignatureContexts.verify(profile, rsa.getPublic(), spx.getPublic(), + try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, rsa.getPublic(), spx.getPublic(), 2 * 1024 * 1024)) { verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch()); verifier.setExpectedTag(sig); @@ -386,7 +395,7 @@ public class HybridSignatureTest { // negative sanity: corrupt classic => must fail (AND) byte[] badClassic = concat(flipOneBit(sub(sig, 0, rsaLen), 0), sub(sig, rsaLen, spxLen)); - try (SignatureContext verifier = HybridSignatureContexts.verify(profile, rsa.getPublic(), spx.getPublic(), + try (SignatureContext verifier = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, rsa.getPublic(), spx.getPublic(), 2 * 1024 * 1024)) { verifier.setVerificationApproach(verifier.getVerificationCore().getThrowOnMismatch()); verifier.setExpectedTag(badClassic); @@ -416,8 +425,11 @@ public class HybridSignatureTest { byte[] msg = randomBytes(size); System.out.println("...msg=" + msg.length + " bytes"); - KeyPair ed = CryptoAlgorithms.require("Ed25519").generateKeyPair(); - KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair(); + ZeroEchoSession session = new ZeroEchoSession(); + KeyPair ed = session.keyBuilders().asymmetric().generateKeyPair("Ed25519", + Ed25519KeyGenSpec.defaultSpec()); + KeyPair spx = session.keyBuilders().asymmetric().generateKeyPair("SPHINCS+", + SphincsPlusKeyGenSpec.defaultSpec()); HybridSignatureProfile profile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null, HybridSignatureProfile.VerifyRule.AND); @@ -425,7 +437,7 @@ public class HybridSignatureTest { byte[] out; int tagLen; - try (SignatureContext tagEnc = HybridSignatureContexts.sign(profile, ed.getPrivate(), spx.getPrivate(), + try (SignatureContext tagEnc = HybridSignatureContexts.sign(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPrivate(), spx.getPrivate(), 2 * 1024 * 1024)) { DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) @@ -437,7 +449,7 @@ public class HybridSignatureTest { System.out.println("...out=" + out.length + " bytes"); - try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(), + try (SignatureContext tagDec = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPublic(), spx.getPublic(), 2 * 1024 * 1024)) { tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch()); @@ -468,8 +480,11 @@ public class HybridSignatureTest { msg = Arrays.copyOf(msg, size); System.out.println("...msg=" + msg.length + " bytes"); - KeyPair ed = CryptoAlgorithms.require("Ed25519").generateKeyPair(); - KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair(); + ZeroEchoSession session = new ZeroEchoSession(); + KeyPair ed = session.keyBuilders().asymmetric().generateKeyPair("Ed25519", + Ed25519KeyGenSpec.defaultSpec()); + KeyPair spx = session.keyBuilders().asymmetric().generateKeyPair("SPHINCS+", + SphincsPlusKeyGenSpec.defaultSpec()); int edLen = tagLen("Ed25519", KeyUsage.SIGN, ed.getPrivate(), null); int spxLen = tagLen("SPHINCS+", KeyUsage.SIGN, spx.getPrivate(), null); @@ -481,7 +496,7 @@ public class HybridSignatureTest { byte[] out; int tagLen; - try (SignatureContext tagEnc = HybridSignatureContexts.sign(profile, ed.getPrivate(), spx.getPrivate(), + try (SignatureContext tagEnc = HybridSignatureContexts.sign(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPrivate(), spx.getPrivate(), 2 * 1024 * 1024)) { DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) @@ -500,7 +515,7 @@ public class HybridSignatureTest { byte[] badClassic = concat(flipOneBit(sub(tag, 0, edLen), 0), sub(tag, edLen, spxLen)); byte[] outBadClassic = concat(body, badClassic); - try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(), + try (SignatureContext tagDec = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPublic(), spx.getPublic(), 2 * 1024 * 1024)) { tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch()); @@ -518,7 +533,7 @@ public class HybridSignatureTest { byte[] badPqc = concat(sub(tag, 0, edLen), flipOneBit(sub(tag, edLen, spxLen), 0)); byte[] outBadPqc = concat(body, badPqc); - try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(), + try (SignatureContext tagDec = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPublic(), spx.getPublic(), 2 * 1024 * 1024)) { tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch()); @@ -536,7 +551,7 @@ public class HybridSignatureTest { byte[] badBoth = concat(flipOneBit(sub(tag, 0, edLen), 0), flipOneBit(sub(tag, edLen, spxLen), 0)); byte[] outBadBoth = concat(body, badBoth); - try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(), + try (SignatureContext tagDec = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPublic(), spx.getPublic(), 2 * 1024 * 1024)) { tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch()); @@ -564,15 +579,17 @@ public class HybridSignatureTest { byte[] msg = randomBytes(size); System.out.println("...msg=" + msg.length + " bytes"); - KeyPair rsa = CryptoAlgorithms.require("RSA").generateKeyPair(); - KeyPair spx = CryptoAlgorithms.require("SPHINCS+").generateKeyPair(); + ZeroEchoSession session = new ZeroEchoSession(); + KeyPair rsa = session.keyBuilders().asymmetric().generateKeyPair("RSA", RsaKeyGenSpec.rsa2048()); + KeyPair spx = session.keyBuilders().asymmetric().generateKeyPair("SPHINCS+", + SphincsPlusKeyGenSpec.defaultSpec()); HybridSignatureProfile profile = new HybridSignatureProfile("RSA", "SPHINCS+", null, null, HybridSignatureProfile.VerifyRule.AND); byte[] out; - try (SignatureContext tagEnc = HybridSignatureContexts.sign(profile, rsa.getPrivate(), spx.getPrivate(), + try (SignatureContext tagEnc = HybridSignatureContexts.sign(new zeroecho.sdk.ZeroEchoSession(), profile, rsa.getPrivate(), spx.getPrivate(), 2 * 1024 * 1024)) { DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) @@ -583,7 +600,7 @@ public class HybridSignatureTest { System.out.println("...out=" + out.length + " bytes"); - try (SignatureContext tagDec = HybridSignatureContexts.verify(profile, rsa.getPublic(), spx.getPublic(), + try (SignatureContext tagDec = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, rsa.getPublic(), spx.getPublic(), 2 * 1024 * 1024)) { tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch()); diff --git a/lib/src/test/java/zeroecho/sdk/util/PasswordTest.java b/lib/src/test/java/zeroecho/sdk/util/PasswordTest.java new file mode 100644 index 0000000..94bf3c7 --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/util/PasswordTest.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + ******************************************************************************/ +package zeroecho.sdk.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.junit.jupiter.api.Test; + +class PasswordTest { + @Test + void canonicalRandomFacadeValidatesNullAndAcceptsEmptyArrays() { + System.out.print("Password/random-boundaries..."); + assertThrows(NullPointerException.class, () -> Password.generateRandom(null)); + byte[] empty = new byte[0]; + assertSame(empty, Password.generateRandom(empty)); + assertThrows(IllegalArgumentException.class, () -> Password.generatePrintablePasswordChars(0)); + assertThrows(IllegalArgumentException.class, () -> Password.generatePrintablePasswordChars(-1)); + + char[] printable = Password.generatePrintablePasswordChars(128); + assertEquals(128, printable.length); + for (char character : printable) { + assertTrue(character >= '!' && character <= '~'); + } + System.out.println("ok"); + } + + @Test + void sharedSourceSupportsParallelUse() throws Exception { + System.out.print("RandomSupport/concurrent..."); + SecureRandom source = RandomSupport.getRandom(); + assertSame(source, RandomSupport.getRandom()); + ExecutorService executor = Executors.newFixedThreadPool(8); + try { + List> futures = new ArrayList<>(); + for (int i = 0; i < 64; i++) { + futures.add(executor.submit(() -> { + assertSame(source, RandomSupport.getRandom()); + return Password.generateRandom(new byte[32]); + })); + } + byte[] first = futures.get(0).get(); + boolean differentOutputObserved = false; + for (int i = 1; i < futures.size(); i++) { + byte[] output = futures.get(i).get(); + if (!java.util.Arrays.equals(first, output)) { + differentOutputObserved = true; + } + } + assertTrue(differentOutputObserved); + assertNotEquals(0, first.length); + } finally { + executor.shutdownNow(); + } + System.out.println("ok"); + } +} diff --git a/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflow.java b/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflow.java index ee07494..17b8034 100644 --- a/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflow.java +++ b/pki/src/main/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflow.java @@ -51,7 +51,6 @@ import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.KeyUsage; import zeroecho.core.alg.common.sig.SignatureInteropProfile; import zeroecho.core.alg.common.sig.SignatureInteropProfiles; @@ -67,6 +66,7 @@ import zeroecho.core.io.TailStrippingInputStream; import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.ContextSpec; import zeroecho.core.storage.KeyringStore; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.pki.api.EncodedObject; import zeroecho.pki.api.Encoding; import zeroecho.pki.api.KeyRef; @@ -82,7 +82,7 @@ import zeroecho.pki.spi.crypto.SignatureWorkflow; * {@link KeyringStore}. It resolves opaque {@link KeyRef} values to provider- * local keyring aliases, materializes the required key objects inside this * boundary, and performs signing or verification through - * {@link CryptoAlgorithms} and {@link SignatureContext}. + * the explicit {@link ZeroEchoSession} and {@link SignatureContext}. *

      * *

      @@ -221,6 +221,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow { / private final java.nio.file.Path keyringPath; private final String keyRefPrefix; private final boolean requireComponentSuffix; + private final ZeroEchoSession session; private final Map statuses; private final Map sinks; @@ -242,6 +243,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow { / this.keyringPath = keyringPath; this.keyRefPrefix = keyRefPrefix; this.requireComponentSuffix = requireComponentSuffix; + this.session = new ZeroEchoSession(); this.statuses = Collections.synchronizedMap(new HashMap<>()); this.sinks = Collections.synchronizedMap(new HashMap<>()); @@ -268,7 +270,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow { / @Override public Set supportedAlgorithms() { // Informational only; runtime policy/key availability may still reject. - Set supported = new java.util.LinkedHashSet<>(CryptoAlgorithms.available()); + Set supported = new java.util.LinkedHashSet<>(session.available()); supported.addAll(SignatureInteropProfiles.algorithmIds()); return Collections.unmodifiableSet(supported); } @@ -559,7 +561,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow { / if (ks != null) { return ks; } - KeyringStore loaded = KeyringStore.load(this.keyringPath); + KeyringStore loaded = KeyringStore.load(this.session, this.keyringPath); this.keyringOrNull = loaded; return loaded; } @@ -626,7 +628,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow { / AlgorithmKeySpec spec = createPublicKeySpecOrThrow(request.algorithmId(), spki); String keyAlg = keyAlgorithmId(request.algorithmId()); try { - return CryptoAlgorithms.importPublic(keyAlg, spec); + return importPublic(keyAlg, spec); } catch (GeneralSecurityException ex) { throw new InvalidRequestException(DC_CRYPTO_FAILURE, ex); } @@ -635,6 +637,13 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow { / throw new InvalidRequestException(DC_UNSUPPORTED_PUBLICKEY_FORM); } + private PublicKey importPublic(String algorithmId, S spec) + throws GeneralSecurityException { + @SuppressWarnings("unchecked") + Class specType = (Class) spec.getClass(); + return session.keyBuilders().asymmetric().publicImporter(algorithmId, specType).importPublic(spec); + } + private static byte[] decodePublicKeyOrThrow(EncodedObject publicKey) throws InvalidRequestException { if (publicKey.encoding() == Encoding.DER || publicKey.encoding() == Encoding.BINARY) { return publicKey.bytes().clone(); @@ -697,7 +706,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow { / return a; } - private static byte[] signStreaming(String algorithmId, PrivateKey prv, PublicKey pub, byte[] msg) + private byte[] signStreaming(String algorithmId, PrivateKey prv, PublicKey pub, byte[] msg) throws GeneralSecurityException, IOException { Optional profile = SignatureInteropProfiles.resolve(algorithmId); @@ -705,12 +714,12 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow { / ContextSpec contextSpec = profile.map(SignatureInteropProfile::contextSpec).orElse(null); int sigLen; - try (SignatureContext verifier = CryptoAlgorithms.create(contextAlgorithmId, KeyUsage.VERIFY, pub, + try (SignatureContext verifier = session.createContext(contextAlgorithmId, KeyUsage.VERIFY, pub, contextSpec)) { sigLen = verifier.tagLength(); } - try (SignatureContext signer = CryptoAlgorithms.create(contextAlgorithmId, KeyUsage.SIGN, prv, contextSpec)) { + try (SignatureContext signer = session.createContext(contextAlgorithmId, KeyUsage.SIGN, prv, contextSpec)) { final byte[][] sigHolder = new byte[1][]; try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(msg)), sigLen, 8192) { @@ -733,7 +742,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow { / } } - private static boolean verifyStreaming(String algorithmId, PublicKey pub, byte[] msg, byte[] signature) + private boolean verifyStreaming(String algorithmId, PublicKey pub, byte[] msg, byte[] signature) throws GeneralSecurityException, IOException { Optional profile = SignatureInteropProfiles.resolve(algorithmId); @@ -744,7 +753,7 @@ public final class ZeroEchoLibSignatureWorkflow implements SignatureWorkflow { / internalSignature = profile.get().externalToInternalSignature(signature); } - try (SignatureContext verifier = CryptoAlgorithms.create(contextAlgorithmId, KeyUsage.VERIFY, pub, + try (SignatureContext verifier = session.createContext(contextAlgorithmId, KeyUsage.VERIFY, pub, contextSpec)) { verifier.setExpectedTag(internalSignature); try (InputStream in = verifier.wrap(new ByteArrayInputStream(msg))) { diff --git a/pki/src/test/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowVerifyEncodedEcdsaTest.java b/pki/src/test/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowVerifyEncodedEcdsaTest.java index 99fa845..06562e1 100644 --- a/pki/src/test/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowVerifyEncodedEcdsaTest.java +++ b/pki/src/test/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowVerifyEncodedEcdsaTest.java @@ -69,7 +69,7 @@ public final class ZeroEchoLibSignatureWorkflowVerifyEncodedEcdsaTest { System.out.println("verifyFromSpkiDerEcdsaSucceeds"); Path keyring = tempDir.resolve("keyring.txt"); - KeyringStore ks = new KeyringStore(); + KeyringStore ks = new KeyringStore(new zeroecho.sdk.ZeroEchoSession()); ks.save(keyring); try (ZeroEchoLibSignatureWorkflow wf = new ZeroEchoLibSignatureWorkflow("wf", keyring, "zeroecho-lib:", true)) { diff --git a/pki/src/test/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowVerifyEncodedTest.java b/pki/src/test/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowVerifyEncodedTest.java index 0632e81..5b90e2e 100644 --- a/pki/src/test/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowVerifyEncodedTest.java +++ b/pki/src/test/java/zeroecho/pki/impl/crypto/zeroecholib/ZeroEchoLibSignatureWorkflowVerifyEncodedTest.java @@ -64,7 +64,7 @@ public final class ZeroEchoLibSignatureWorkflowVerifyEncodedTest { System.out.println("verifyFromSpkiDerSucceeds"); Path keyring = tempDir.resolve("keyring.txt"); - KeyringStore ks = new KeyringStore(); + KeyringStore ks = new KeyringStore(new zeroecho.sdk.ZeroEchoSession()); ks.save(keyring); try (ZeroEchoLibSignatureWorkflow wf = new ZeroEchoLibSignatureWorkflow("wf", keyring, "zeroecho-lib:", true)) { diff --git a/samples/src/test/java/demo/AesTest.java b/samples/src/test/java/demo/AesTest.java index 4c4c8b1..f73b8c3 100644 --- a/samples/src/test/java/demo/AesTest.java +++ b/samples/src/test/java/demo/AesTest.java @@ -50,7 +50,6 @@ import conflux.Ctx; import conflux.CtxInterface; import zeroecho.core.ConfluxKeys; import zeroecho.core.CryptoAlgorithm; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.KeyUsage; import zeroecho.core.alg.aes.AesKeyGenSpec; import zeroecho.core.alg.aes.AesSpec; @@ -61,23 +60,16 @@ import zeroecho.sdk.builders.alg.AesDataContentBuilder; import zeroecho.sdk.builders.core.DataContentChainBuilder; import zeroecho.sdk.builders.core.PlainBytesBuilder; import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.ZeroEchoSession; class AesTest { private static final Logger LOG = Logger.getLogger(AesTest.class.getName()); + private final ZeroEchoSession zeroEchoSession = new ZeroEchoSession(); SecretKey generateAesKey() throws GeneralSecurityException { - // Locate the AES algorithm in the catalog - CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); - SecretKey key = aes - // Retrieve the builder that works with AesKeyGenSpec - the specification for - // AES key generation - .symmetricKeyBuilder(AesKeyGenSpec.class) - // Generate a secret key according to the AES256 specification - .generateSecret(AesKeyGenSpec.aes256()); - // Log the generated key (truncated to short hex for readability) - LOG.log(Level.INFO, "AES256 key generated: {0}", Strings.toShortHexString(key.getEncoded())); - - // or just: CryptoAlgorithms.generateSecret("AES", AesKeyGenSpec.aes256()) + SecretKey key = zeroEchoSession.keyBuilders().symmetric() + .generate("AES", AesKeyGenSpec.aes256()); + LOG.log(Level.INFO, "AES256 key generated"); return key; } @@ -101,7 +93,7 @@ class AesTest { SecretKey key = generateAesKey(); byte[] encrypted; // Request an encryption context using the key and AES-GCM-128 specification - try (EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec)) { + try (EncryptionContext enc = zeroEchoSession.createContext("AES", KeyUsage.ENCRYPT, key, spec)) { // This context implements ContextAware, allowing us to associate our session ((ContextAware) enc).setContext(session); // Get an encrypted stream that processes the plaintext on-the-fly @@ -132,7 +124,7 @@ class AesTest { // Separate context again to hold IV and AAD values so we can inspect them later CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime()); - AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder() + AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()) // Use the generated AES key .importKeyRaw(key.getEncoded()) // Specify AES-GCM-128 mode @@ -171,7 +163,7 @@ class AesTest { // Sample message to encrypt byte[] msg = randomBytes(100); - AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder() + AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()) // Automatically generate a 256-bit AES key .generateKey(256) // Store ad-hoc generated parameters (IV, AAD) in the stream header @@ -210,7 +202,7 @@ class AesTest { // Sample message to encrypt byte[] msg = randomBytes(100); - AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder().generateKey(256).modeGcm(128).withHeader(); + AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).generateKey(256).modeGcm(128).withHeader(); // The builder stores generated IV and AAD inside the stream header DataContent dccb = DataContentChainBuilder.encrypt().add(PlainBytesBuilder.builder().bytes(msg)).add(aesBuilder) @@ -227,7 +219,7 @@ class AesTest { dccb = DataContentChainBuilder.decrypt().add(PlainBytesBuilder.builder().bytes(encrypted)) // Use the same AES key for decryption; IV and AAD are restored from the header - .add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()).modeGcm(128).withHeader()) + .add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).importKeyRaw(key.getEncoded()).modeGcm(128).withHeader()) // Build the pipeline .build(); byte[] decrypted; diff --git a/samples/src/test/java/demo/AgreementVariantsTest.java b/samples/src/test/java/demo/AgreementVariantsTest.java index a4a47ad..d959018 100644 --- a/samples/src/test/java/demo/AgreementVariantsTest.java +++ b/samples/src/test/java/demo/AgreementVariantsTest.java @@ -52,6 +52,7 @@ import zeroecho.core.alg.xdh.XdhSpec; import zeroecho.core.context.AgreementContext; import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.spec.VoidSpec; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.core.util.Strings; import zeroecho.sdk.util.BouncyCastleActivator; @@ -107,6 +108,7 @@ import zeroecho.sdk.util.BouncyCastleActivator; *

      */ class AgreementVariantsTest { + private final ZeroEchoSession session = new ZeroEchoSession(); private static final Logger LOG = Logger.getLogger(AgreementVariantsTest.class.getName()); @@ -138,16 +140,17 @@ class AgreementVariantsTest { void kemAdapter_mlKem_roundTrip() throws Exception { LOG.info("kemAdapter_mlKem_roundTrip - KEM_ADAPTER (ML-KEM)"); - KeyPair recipient = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber1024()); + KeyPair recipient = session.keyBuilders().asymmetric() + .generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber1024()); MessageAgreementContext initiator = null; MessageAgreementContext responder = null; try { // Initiator: constructed with recipient's public key (encapsulation side). - initiator = CryptoAlgorithms.create("ML-KEM", KeyUsage.AGREEMENT, recipient.getPublic(), VoidSpec.INSTANCE); + initiator = session.createContext("ML-KEM", KeyUsage.AGREEMENT, recipient.getPublic(), VoidSpec.INSTANCE); // Responder: constructed with recipient's private key (decapsulation side). - responder = CryptoAlgorithms.create("ML-KEM", KeyUsage.AGREEMENT, recipient.getPrivate(), + responder = session.createContext("ML-KEM", KeyUsage.AGREEMENT, recipient.getPrivate(), VoidSpec.INSTANCE); // One-shot outbound message: KEM ciphertext / encapsulation payload. @@ -193,17 +196,16 @@ class AgreementVariantsTest { void classicAgreement_x25519_roundTrip() throws Exception { LOG.info("classicAgreement_x25519_roundTrip - CLASSIC_AGREEMENT (X25519)"); - CryptoAlgorithm xdh = CryptoAlgorithms.require("Xdh"); - KeyPair alice = xdh.generateKeyPair(); - KeyPair bob = xdh.generateKeyPair(); + KeyPair alice = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bob = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); AgreementContext aCtx = null; AgreementContext bCtx = null; try { // Both contexts are built from local private keys. - aCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, alice.getPrivate(), XdhSpec.X25519); - bCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, bob.getPrivate(), XdhSpec.X25519); + aCtx = session.createContext("Xdh", KeyUsage.AGREEMENT, alice.getPrivate(), XdhSpec.X25519); + bCtx = session.createContext("Xdh", KeyUsage.AGREEMENT, bob.getPrivate(), XdhSpec.X25519); // The protocol layer provides peer public keys (here we use in-memory // exchange). @@ -254,9 +256,8 @@ class AgreementVariantsTest { void pairMessage_x25519_roundTrip() throws Exception { LOG.info("pairMessage_x25519_roundTrip - PAIR_MESSAGE (X25519)"); - CryptoAlgorithm xdh = CryptoAlgorithms.require("Xdh"); - KeyPair alice = xdh.generateKeyPair(); - KeyPair bob = xdh.generateKeyPair(); + KeyPair alice = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bob = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); // Wrapper is required because ZeroEcho capability dispatch uses Key (KeyPair is // not a Key). @@ -267,8 +268,8 @@ class AgreementVariantsTest { MessageAgreementContext bCtx = null; try { - aCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, aliceKey, XdhSpec.X25519); - bCtx = CryptoAlgorithms.create("Xdh", KeyUsage.AGREEMENT, bobKey, XdhSpec.X25519); + aCtx = session.createContext("Xdh", KeyUsage.AGREEMENT, aliceKey, XdhSpec.X25519); + bCtx = session.createContext("Xdh", KeyUsage.AGREEMENT, bobKey, XdhSpec.X25519); // Outbound messages: SPKI encodings of local public keys. byte[] aMsg = aCtx.getPeerMessage(); diff --git a/samples/src/test/java/demo/CombinedDeliveryTest.java b/samples/src/test/java/demo/CombinedDeliveryTest.java index 0aad558..c7cf2b6 100644 --- a/samples/src/test/java/demo/CombinedDeliveryTest.java +++ b/samples/src/test/java/demo/CombinedDeliveryTest.java @@ -46,7 +46,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.KeyUsage; import zeroecho.core.alg.elgamal.ElgamalParamSpec; import zeroecho.core.alg.kyber.KyberKeyGenSpec; @@ -58,11 +57,15 @@ import zeroecho.sdk.builders.core.PlainBytesBuilder; import zeroecho.sdk.content.api.DataContent; import zeroecho.sdk.guard.MultiRecipientDataSourceBuilder; import zeroecho.sdk.guard.UnlockMaterial; +import zeroecho.sdk.Pbkdf2Limits; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.sdk.util.BouncyCastleActivator; @TestInstance(TestInstance.Lifecycle.PER_CLASS) class CombinedDeliveryTest { private static final Logger LOG = Logger.getLogger(CombinedDeliveryTest.class.getName()); + private final ZeroEchoSession session = new ZeroEchoSession() + .withPbkdf2Limits(new Pbkdf2Limits(1_000_000, 1_000_000)); @BeforeAll void setupProviders() { @@ -70,31 +73,25 @@ class CombinedDeliveryTest { } KeyPair generateKyberKeys() throws GeneralSecurityException { - KeyPair kp = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber1024()); - - LOG.log(Level.INFO, "ML-KEM key public={0} private={1}", - new Object[] { Strings.toShortHexString(kp.getPublic().getEncoded()), - Strings.toShortHexString(kp.getPrivate().getEncoded()) }); + KeyPair kp = session.keyBuilders().asymmetric() + .generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber1024()); + LOG.log(Level.INFO, "ML-KEM key pair generated"); return kp; } KeyPair generateRsaKeys() throws GeneralSecurityException { - KeyPair kp = CryptoAlgorithms.generateKeyPair("RSA", RsaKeyGenSpec.rsa4096()); - - LOG.log(Level.INFO, "RSA key public={0} private={1}", - new Object[] { Strings.toShortHexString(kp.getPublic().getEncoded()), - Strings.toShortHexString(kp.getPrivate().getEncoded()) }); + KeyPair kp = session.keyBuilders().asymmetric() + .generateKeyPair("RSA", RsaKeyGenSpec.rsa4096()); + LOG.log(Level.INFO, "RSA key pair generated"); return kp; } KeyPair generateElGamalKeys() throws GeneralSecurityException { - KeyPair kp = CryptoAlgorithms.generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); - - LOG.log(Level.INFO, "ElGamal key public={0} private={1}", - new Object[] { Strings.toShortHexString(kp.getPublic().getEncoded()), - Strings.toShortHexString(kp.getPrivate().getEncoded()) }); + KeyPair kp = session.keyBuilders().asymmetric() + .generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + LOG.log(Level.INFO, "ElGamal key pair generated"); return kp; } @@ -112,16 +109,16 @@ class CombinedDeliveryTest { KeyPair decoy1rsa = generateRsaKeys(); KeyPair decoy2rsa = generateRsaKeys(); - AesDataContentBuilder payload = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + AesDataContentBuilder payload = AesDataContentBuilder.builder(session).modeCbcPkcs5().withHeader(); - MultiRecipientDataSourceBuilder multi = MultiRecipientDataSourceBuilder.builder() + MultiRecipientDataSourceBuilder multi = MultiRecipientDataSourceBuilder.builder(session) // AES-256 - 32 bytes of key material .payloadKeyBytes(32).withAes(payload) // add recipients - the context initialized with the public key - .addRecipient(CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic())) - .addRecipient(CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic())) + .addRecipient(session.createContext("ElGamal", KeyUsage.ENCRYPT, elg.getPublic())) + .addRecipient(session.createContext("RSA", KeyUsage.ENCRYPT, rsa.getPublic())) // ML-KEM PostQuantum uses AES for the inner payload - .addRecipient(CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kem.getPublic()), + .addRecipient(session.createContext("ML-KEM", KeyUsage.ENCAPSULATE, kem.getPublic()), /* AES256 key size */ 32, /* salt size in hkdf */ @@ -129,8 +126,8 @@ class CombinedDeliveryTest { // Password (via KDF) .addPasswordRecipient(password, /* iterations */ 200_000, /* saltLen */ 16, /* kekBytes */ 32) // and some decoys - .addRecipientDecoy(CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, decoy1rsa.getPublic())) - .addRecipientDecoy(CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, decoy2rsa.getPublic())); + .addRecipientDecoy(session.createContext("RSA", KeyUsage.ENCRYPT, decoy1rsa.getPublic())) + .addRecipientDecoy(session.createContext("RSA", KeyUsage.ENCRYPT, decoy2rsa.getPublic())); // shuffle all the recipients multi.shuffle(); @@ -153,8 +150,8 @@ class CombinedDeliveryTest { private void recipientProcessing(String method, byte[] msg, byte[] encrypted, UnlockMaterial unlock) throws IOException { - AesDataContentBuilder payload = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); - MultiRecipientDataSourceBuilder multi = MultiRecipientDataSourceBuilder.builder() + AesDataContentBuilder payload = AesDataContentBuilder.builder(session).modeCbcPkcs5().withHeader(); + MultiRecipientDataSourceBuilder multi = MultiRecipientDataSourceBuilder.builder(session) // define our payload .payloadKeyBytes(32).withAes(payload) // one recipient diff --git a/samples/src/test/java/demo/HybridDerivedAesDemoTest.java b/samples/src/test/java/demo/HybridDerivedAesDemoTest.java index 578d46e..c9e2598 100644 --- a/samples/src/test/java/demo/HybridDerivedAesDemoTest.java +++ b/samples/src/test/java/demo/HybridDerivedAesDemoTest.java @@ -44,7 +44,6 @@ import java.util.logging.Logger; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.alg.kyber.KyberKeyGenSpec; import zeroecho.core.alg.xdh.XdhSpec; import zeroecho.sdk.builders.HybridKexBuilder; @@ -56,6 +55,7 @@ import zeroecho.sdk.hybrid.derived.HybridDerived; import zeroecho.sdk.hybrid.kex.HybridKexContext; import zeroecho.sdk.hybrid.kex.HybridKexExporter; import zeroecho.sdk.hybrid.kex.HybridKexProfile; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.sdk.hybrid.kex.HybridKexTranscript; import zeroecho.sdk.util.BouncyCastleActivator; @@ -78,6 +78,7 @@ import zeroecho.sdk.util.BouncyCastleActivator; *

      */ class HybridDerivedAesDemoTest { + private final ZeroEchoSession session = new ZeroEchoSession(); private static final Logger LOG = Logger.getLogger(HybridDerivedAesDemoTest.class.getName()); @@ -108,16 +109,17 @@ class HybridDerivedAesDemoTest { "hybrid-derived-aes-gcm-condensed"); // ...Generate classic key pairs for X25519 (Xdh + XdhSpec.X25519). - KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair aliceClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); // ...Generate PQC key pair for ML-KEM-768 (recipient; used by Bob side to // decapsulate). - KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair bobPqc = session.keyBuilders().asymmetric() + .generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); // ...Build Alice initiator: classic agreement (out-of-band peer pub) + PQC // encapsulation. - HybridKexContext alice = HybridKexBuilder.builder() + HybridKexContext alice = HybridKexBuilder.builder(session) // ...Set mandatory profile. .profile(profile) // ...Bind builder HKDF info to transcript. @@ -142,7 +144,7 @@ class HybridDerivedAesDemoTest { .buildInitiator(); // ...Build Bob responder: classic agreement + PQC decapsulation. - HybridKexContext bob = HybridKexBuilder.builder() + HybridKexContext bob = HybridKexBuilder.builder(session) // ...Set mandatory profile. .profile(profile) // ...Bind builder HKDF info to transcript. @@ -203,7 +205,7 @@ class HybridDerivedAesDemoTest { // ...Inject explicit AAD. .aad(aad) // ...Apply derived key(256b) and IV(12B) to AES-GCM with header. - .applyToAesGcm(AesDataContentBuilder.builder() + .applyToAesGcm(AesDataContentBuilder.builder(session) // ...Store IV in header for decrypt side. .withHeader() // ...Use AES-GCM with 128-bit authentication tag. @@ -230,7 +232,7 @@ class HybridDerivedAesDemoTest { // ...Same explicit AAD as encryption. .aad(aad) // ...Apply derived key and IV to AES-GCM with header. - .applyToAesGcm(AesDataContentBuilder.builder() + .applyToAesGcm(AesDataContentBuilder.builder(session) // ...Parse IV from header. .withHeader() // ...Use AES-GCM with 128-bit authentication tag. @@ -271,14 +273,15 @@ class HybridDerivedAesDemoTest { "hybrid-derived-aes-gcm-expanded"); // ...Generate classic key pairs for X25519. - KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair aliceClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); // ...Generate PQC key pair for ML-KEM-768. - KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair bobPqc = session.keyBuilders().asymmetric() + .generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); // ...Build Alice initiator in a step-by-step manner. - HybridKexBuilder aliceBuilder = HybridKexBuilder.builder(); + HybridKexBuilder aliceBuilder = HybridKexBuilder.builder(session); // ...Set mandatory profile. aliceBuilder.profile(profile); // ...Bind builder HKDF info to transcript. @@ -306,7 +309,7 @@ class HybridDerivedAesDemoTest { HybridKexContext alice = alicePqcCfg.buildInitiator(); // ...Build Bob responder in a step-by-step manner. - HybridKexBuilder bobBuilder = HybridKexBuilder.builder(); + HybridKexBuilder bobBuilder = HybridKexBuilder.builder(session); // ...Set mandatory profile. bobBuilder.profile(profile); // ...Bind builder HKDF info to transcript. @@ -356,7 +359,7 @@ class HybridDerivedAesDemoTest { byte[] aad = "aad:demo:expanded".getBytes(StandardCharsets.UTF_8); // ...Prepare AES builder for encryption. - AesDataContentBuilder aesEnc = AesDataContentBuilder.builder(); + AesDataContentBuilder aesEnc = AesDataContentBuilder.builder(session); // ...Store IV in header. aesEnc.withHeader(); // ...Use AES-GCM with 128-bit authentication tag. @@ -389,7 +392,7 @@ class HybridDerivedAesDemoTest { System.out.println("...ciphertext " + lens(ciphertext) + " " + shortHex(ciphertext, 48)); // ...Prepare AES builder for decryption. - AesDataContentBuilder aesDec = AesDataContentBuilder.builder(); + AesDataContentBuilder aesDec = AesDataContentBuilder.builder(session); // ...Parse IV from header. aesDec.withHeader(); // ...Use AES-GCM with 128-bit authentication tag. @@ -453,13 +456,14 @@ class HybridDerivedAesDemoTest { byte[] aad = "aad:local-self".getBytes(StandardCharsets.UTF_8); // ...Generate classic identity keys (X25519). - KeyPair selfClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair selfClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); // ...Generate PQC identity keys (ML-KEM-768). - KeyPair selfPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair selfPqc = session.keyBuilders().asymmetric() + .generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); // ...Build local initiator (encapsulation) against our own public keys. - HybridKexContext encKex = HybridKexBuilder.builder() + HybridKexContext encKex = HybridKexBuilder.builder(session) // ...Set mandatory profile. .profile(profile) // ...Bind derivation to transcript. @@ -509,7 +513,7 @@ class HybridDerivedAesDemoTest { // ...Inject explicit AAD. .aad(aad) // ...Apply derived key(256b) and IV(12B) to AES-GCM with header. - .applyToAesGcm(AesDataContentBuilder.builder() + .applyToAesGcm(AesDataContentBuilder.builder(session) // ...Store IV in header for decrypt side. .withHeader() // ...Use AES-GCM with 128-bit authentication tag. @@ -525,7 +529,7 @@ class HybridDerivedAesDemoTest { // ...Build local responder (decapsulation) using our own private keys and // stored peer message. - HybridKexContext decKex = HybridKexBuilder.builder() + HybridKexContext decKex = HybridKexBuilder.builder(session) // ...Set mandatory profile. .profile(profile) // ...Bind derivation to transcript. @@ -577,7 +581,7 @@ class HybridDerivedAesDemoTest { // ...Same explicit AAD. .aad(aad) // ...Apply derived key and IV to AES-GCM with header. - .applyToAesGcm(AesDataContentBuilder.builder() + .applyToAesGcm(AesDataContentBuilder.builder(session) // ...Parse IV from header. .withHeader() // ...Use AES-GCM with 128-bit authentication tag. diff --git a/samples/src/test/java/demo/HybridKexDemoTest.java b/samples/src/test/java/demo/HybridKexDemoTest.java index 0a5532f..282e33a 100644 --- a/samples/src/test/java/demo/HybridKexDemoTest.java +++ b/samples/src/test/java/demo/HybridKexDemoTest.java @@ -42,13 +42,13 @@ import java.util.logging.Logger; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.alg.common.agreement.KeyPairKey; import zeroecho.core.alg.kyber.KyberKeyGenSpec; import zeroecho.core.alg.xdh.XdhSpec; import zeroecho.sdk.hybrid.kex.HybridKexContext; import zeroecho.sdk.hybrid.kex.HybridKexContexts; import zeroecho.sdk.hybrid.kex.HybridKexProfile; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.sdk.util.BouncyCastleActivator; /** @@ -103,6 +103,7 @@ import zeroecho.sdk.util.BouncyCastleActivator; *

      */ class HybridKexDemoTest { + private final ZeroEchoSession session = new ZeroEchoSession(); private static final Logger LOG = Logger.getLogger(HybridKexDemoTest.class.getName()); @@ -121,11 +122,12 @@ class HybridKexDemoTest { logBegin("CLASSIC_AGREEMENT + KEM_ADAPTER", "Xdh/X25519 + ML-KEM-768", "HKDF-SHA256", "OKM=32B"); // Classic leg keys (Xdh + XdhSpec.X25519). - KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair aliceClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); // PQC leg: Bob is the KEM recipient (has ML-KEM keypair). - KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair bobPqc = session.keyBuilders().asymmetric() + .generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); // Hybrid profile: default HKDF label, 32-byte output suitable for symmetric // keys. @@ -138,13 +140,13 @@ class HybridKexDemoTest { // Alice (initiator): classic uses Alice private + Bob classic public // (out-of-band). // ...PQC uses Bob PQC public and will produce a KEM ciphertext. - alice = HybridKexContexts.initiator(profile, "Xdh", aliceClassic.getPrivate(), bobClassic.getPublic(), + alice = HybridKexContexts.initiator(session, profile, "Xdh", aliceClassic.getPrivate(), bobClassic.getPublic(), XdhSpec.X25519, "ML-KEM", bobPqc.getPublic(), null); // Bob (responder): classic uses Bob private + Alice classic public // (out-of-band). // ...PQC uses Bob PQC private and will consume Alice's ciphertext. - bob = HybridKexContexts.responder(profile, "Xdh", bobClassic.getPrivate(), aliceClassic.getPublic(), + bob = HybridKexContexts.responder(session, profile, "Xdh", bobClassic.getPrivate(), aliceClassic.getPublic(), XdhSpec.X25519, "ML-KEM", bobPqc.getPrivate(), null); // Alice -> Bob: hybrid message carries PQC ciphertext; classic part is empty. @@ -175,11 +177,12 @@ class HybridKexDemoTest { logBegin("PAIR_MESSAGE + KEM_ADAPTER", "Xdh/X25519 + ML-KEM-768", "HKDF-SHA256", "OKM=32B"); // Classic leg keys (Xdh + XdhSpec.X25519). - KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair aliceClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); // PQC leg: Bob is the KEM recipient (has ML-KEM keypair). - KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair bobPqc = session.keyBuilders().asymmetric() + .generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); HybridKexProfile profile = HybridKexProfile.defaultProfile(32); @@ -190,14 +193,14 @@ class HybridKexDemoTest { // Alice classic leg is message-based (PAIR_MESSAGE): it will emit her public // key as SPKI bytes. // ...PQC leg (KEM initiator) will emit ciphertext. - alice = HybridKexContexts.initiatorPairMessage(profile, "Xdh", new KeyPairKey(aliceClassic), XdhSpec.X25519, + alice = HybridKexContexts.initiatorPairMessage(session, profile, "Xdh", new KeyPairKey(aliceClassic), XdhSpec.X25519, "ML-KEM", bobPqc.getPublic(), null); // Bob classic leg is message-based (PAIR_MESSAGE): it will emit his public key // as SPKI bytes. // ...PQC leg (KEM responder) consumes ciphertext and typically does not emit a // PQC message. - bob = HybridKexContexts.responderPairMessage(profile, "Xdh", new KeyPairKey(bobClassic), XdhSpec.X25519, + bob = HybridKexContexts.responderPairMessage(session, profile, "Xdh", new KeyPairKey(bobClassic), XdhSpec.X25519, "ML-KEM", bobPqc.getPrivate(), null); // Alice -> Bob: hybrid message carries classic SPKI + PQC ciphertext. @@ -231,13 +234,13 @@ class HybridKexDemoTest { logBegin("Builder", "CLASSIC_AGREEMENT + KEM_ADAPTER", "Xdh/X25519 + ML-KEM-768", "HKDF-SHA256", "OKM=32B"); // ...Generate classic leg keys (Xdh + XdhSpec.X25519). - java.security.KeyPair aliceClassic = zeroecho.core.CryptoAlgorithms.generateKeyPair("Xdh", + java.security.KeyPair aliceClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", zeroecho.core.alg.xdh.XdhSpec.X25519); - java.security.KeyPair bobClassic = zeroecho.core.CryptoAlgorithms.generateKeyPair("Xdh", + java.security.KeyPair bobClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", zeroecho.core.alg.xdh.XdhSpec.X25519); // ...Generate PQC (recipient) keys (ML-KEM-768). - java.security.KeyPair bobPqc = zeroecho.core.CryptoAlgorithms.generateKeyPair("ML-KEM", + java.security.KeyPair bobPqc = session.keyBuilders().asymmetric().generateKeyPair("ML-KEM", zeroecho.core.alg.kyber.KyberKeyGenSpec.kyber768()); // ...Create a profile for HKDF (output length 32 bytes). @@ -252,7 +255,7 @@ class HybridKexDemoTest { zeroecho.sdk.hybrid.kex.HybridKexPolicy policy = new zeroecho.sdk.hybrid.kex.HybridKexPolicy(128, 192, 32); // ...Start the builder. - zeroecho.sdk.builders.HybridKexBuilder b = zeroecho.sdk.builders.HybridKexBuilder.builder() + zeroecho.sdk.builders.HybridKexBuilder b = zeroecho.sdk.builders.HybridKexBuilder.builder(session) // ...Set HKDF profile (salt/info/outLen). .profile(profile) // ...Bind HKDF info to transcript (protocol context). @@ -283,7 +286,7 @@ class HybridKexDemoTest { // ...Build responder-side context (Bob) with symmetric configuration (note: PQC // uses private key). - zeroecho.sdk.hybrid.kex.HybridKexContext bob = zeroecho.sdk.builders.HybridKexBuilder.builder() + zeroecho.sdk.hybrid.kex.HybridKexContext bob = zeroecho.sdk.builders.HybridKexBuilder.builder(session) // ...Set the same HKDF profile to derive the same OKM. .profile(profile) // ...Bind the same transcript to ensure both sides derive the same OKM. @@ -346,11 +349,12 @@ class HybridKexDemoTest { logBegin("Builder", "PAIR_MESSAGE + KEM_ADAPTER", "Xdh/X25519 + ML-KEM-768", "HKDF-SHA256", "OKM=32B"); // ...Generate classic leg keys (Xdh + XdhSpec.X25519). - KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); - KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair aliceClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); + KeyPair bobClassic = session.keyBuilders().asymmetric().generateKeyPair("Xdh", XdhSpec.X25519); // ...Generate PQC (recipient) keys (ML-KEM-768). - KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair bobPqc = session.keyBuilders().asymmetric() + .generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768()); // ...Create a profile for HKDF (output length 32 bytes). HybridKexProfile profile = HybridKexProfile.defaultProfile(32); @@ -365,7 +369,7 @@ class HybridKexDemoTest { zeroecho.sdk.hybrid.kex.HybridKexPolicy policy = new zeroecho.sdk.hybrid.kex.HybridKexPolicy(128, 192, 32); // ...Start the initiator builder. - zeroecho.sdk.builders.HybridKexBuilder initBuilder = zeroecho.sdk.builders.HybridKexBuilder.builder() + zeroecho.sdk.builders.HybridKexBuilder initBuilder = zeroecho.sdk.builders.HybridKexBuilder.builder(session) // ...Set HKDF profile (salt/info/outLen). .profile(profile) // ...Bind HKDF info to transcript (protocol context). @@ -395,7 +399,7 @@ class HybridKexDemoTest { .buildInitiator(); // ...Start the responder builder. - zeroecho.sdk.builders.HybridKexBuilder respBuilder = zeroecho.sdk.builders.HybridKexBuilder.builder() + zeroecho.sdk.builders.HybridKexBuilder respBuilder = zeroecho.sdk.builders.HybridKexBuilder.builder(session) // ...Set the same HKDF profile to derive the same OKM. .profile(profile) // ...Bind the same transcript to ensure both sides derive the same OKM. diff --git a/samples/src/test/java/demo/HybridSigningAesTest.java b/samples/src/test/java/demo/HybridSigningAesTest.java index d10e8bf..abf29c8 100644 --- a/samples/src/test/java/demo/HybridSigningAesTest.java +++ b/samples/src/test/java/demo/HybridSigningAesTest.java @@ -48,10 +48,11 @@ import javax.crypto.SecretKey; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import zeroecho.core.CryptoAlgorithm; -import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.alg.ed25519.Ed25519KeyGenSpec; +import zeroecho.core.alg.sphincsplus.SphincsPlusKeyGenSpec; import zeroecho.core.context.SignatureContext; import zeroecho.sdk.builders.TagTrailerDataContentBuilder; +import zeroecho.sdk.ZeroEchoSession; import zeroecho.sdk.builders.alg.AesDataContentBuilder; import zeroecho.sdk.builders.core.DataContentChainBuilder; import zeroecho.sdk.builders.core.PlainBytesBuilder; @@ -99,7 +100,7 @@ class HybridSigningAesTest { byte[] msg = randomBytes(100); // AES-GCM with header, runtime params are stored in header - AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder().generateKey(256).modeGcm(128).withHeader(); + AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).generateKey(256).modeGcm(128).withHeader(); // Hybrid signature: Ed25519 + SPHINCS+ (AND) HybridSignatureProfile profile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null, @@ -108,9 +109,9 @@ class HybridSigningAesTest { KeyPair ed = generateKeyPair("Ed25519"); KeyPair spx = generateKeyPair("SPHINCS+"); - SignatureContext tagEnc = HybridSignatureContexts.sign(profile, ed.getPrivate(), spx.getPrivate(), + SignatureContext tagEnc = HybridSignatureContexts.sign(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPrivate(), spx.getPrivate(), 2 * 1024 * 1024); - SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(), + SignatureContext tagDec = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPublic(), spx.getPublic(), 2 * 1024 * 1024); // For verification, make mismatch behavior explicit (builder also supports @@ -141,7 +142,7 @@ class HybridSigningAesTest { // encrypted input .add(PlainBytesBuilder.builder().bytes(ciphertext)) // AES-GCM decryption - .add(AesDataContentBuilder.builder().importKeyRaw(aesKey.getEncoded()).modeGcm(128).withHeader()) + .add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).importKeyRaw(aesKey.getEncoded()).modeGcm(128).withHeader()) // hybrid signature verification .add(new TagTrailerDataContentBuilder(tagDec).bufferSize(8192).throwOnMismatch()).build(); @@ -161,7 +162,7 @@ class HybridSigningAesTest { byte[] msg = randomBytes(100); // AES-GCM with header, runtime params are stored in header - AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder().generateKey(256).modeGcm(128).withHeader(); + AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).generateKey(256).modeGcm(128).withHeader(); // Hybrid signature: Ed25519 + SPHINCS+ (AND) HybridSignatureProfile profile = new HybridSignatureProfile("Ed25519", "SPHINCS+", null, null, @@ -170,9 +171,9 @@ class HybridSigningAesTest { KeyPair ed = generateKeyPair("Ed25519"); KeyPair spx = generateKeyPair("SPHINCS+"); - SignatureContext tagEnc = HybridSignatureContexts.sign(profile, ed.getPrivate(), spx.getPrivate(), + SignatureContext tagEnc = HybridSignatureContexts.sign(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPrivate(), spx.getPrivate(), 2 * 1024 * 1024); - SignatureContext tagDec = HybridSignatureContexts.verify(profile, ed.getPublic(), spx.getPublic(), + SignatureContext tagDec = HybridSignatureContexts.verify(new zeroecho.sdk.ZeroEchoSession(), profile, ed.getPublic(), spx.getPublic(), 2 * 1024 * 1024); tagDec.setVerificationApproach(tagDec.getVerificationCore().getThrowOnMismatch()); @@ -205,7 +206,7 @@ class HybridSigningAesTest { // hybrid signature verification .add(new TagTrailerDataContentBuilder(tagDec).bufferSize(8192).throwOnMismatch()) // AES-GCM decryption - .add(AesDataContentBuilder.builder().importKeyRaw(aesKey.getEncoded()).modeGcm(128).withHeader()) + .add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).importKeyRaw(aesKey.getEncoded()).modeGcm(128).withHeader()) .build(); byte[] decrypted; @@ -219,8 +220,14 @@ class HybridSigningAesTest { // helpers private static KeyPair generateKeyPair(String algId) throws GeneralSecurityException { - CryptoAlgorithm alg = CryptoAlgorithms.require(algId); - return alg.generateKeyPair(); + ZeroEchoSession session = new ZeroEchoSession(); + return switch (algId) { + case "Ed25519" -> session.keyBuilders().asymmetric().generateKeyPair(algId, + Ed25519KeyGenSpec.defaultSpec()); + case "SPHINCS+" -> session.keyBuilders().asymmetric().generateKeyPair(algId, + SphincsPlusKeyGenSpec.defaultSpec()); + default -> throw new IllegalArgumentException("Unsupported sample signature algorithm: " + algId); + }; } private static byte[] randomBytes(int len) { diff --git a/samples/src/test/java/demo/PostQuantumTest.java b/samples/src/test/java/demo/PostQuantumTest.java index e581f7a..e485d55 100644 --- a/samples/src/test/java/demo/PostQuantumTest.java +++ b/samples/src/test/java/demo/PostQuantumTest.java @@ -47,7 +47,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.alg.kyber.KyberKeyGenSpec; import zeroecho.core.util.Strings; import zeroecho.sdk.builders.alg.AesDataContentBuilder; @@ -56,9 +55,11 @@ import zeroecho.sdk.builders.core.DataContentChainBuilder; import zeroecho.sdk.builders.core.PlainBytesBuilder; import zeroecho.sdk.content.api.DataContent; import zeroecho.sdk.util.BouncyCastleActivator; +import zeroecho.sdk.ZeroEchoSession; @TestInstance(TestInstance.Lifecycle.PER_CLASS) class PostQuantumTest { + private final ZeroEchoSession session = new ZeroEchoSession(); private static final Logger LOG = Logger.getLogger(PostQuantumTest.class.getName()); @BeforeAll @@ -67,7 +68,8 @@ class PostQuantumTest { } KeyPair generateKyberKeys() throws GeneralSecurityException { - KeyPair kp = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber1024()); + KeyPair kp = session.keyBuilders().asymmetric() + .generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber1024()); LOG.log(Level.INFO, "ML-KEM key public={0} private={1}", new Object[] { Strings.toShortHexString(kp.getPublic().getEncoded()), @@ -87,7 +89,7 @@ class PostQuantumTest { // Start the chain with a plain byte array as input .add(PlainBytesBuilder.builder().bytes(msg)) // Add a KEM (Key Encapsulation Mechanism) stage - .add(KemDataContentBuilder.builder() + .add(KemDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()) // Select a KEM-capable algorithm: ML-KEM (Kyber variant) .kem("ML-KEM") // Encrypt to this recipient’s public key @@ -95,7 +97,7 @@ class PostQuantumTest { // Derive a shared secret and expand/obfuscate it using HKDF-SHA256 .hkdfSha256("KEM-demo".getBytes(StandardCharsets.US_ASCII)) // Use the derived secret as a key for AES - .withAes(AesDataContentBuilder.builder() + .withAes(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()) // Configure AES in GCM mode with a 128-bit authentication tag .modeGcm(128) // Store auxiliary parameters (e.g., random IV) inside the stream header @@ -113,7 +115,7 @@ class PostQuantumTest { // Start with the encrypted byte array as input .add(PlainBytesBuilder.builder().bytes(encrypted)) // Add the KEM stage again, matching the encryption parameters - .add(KemDataContentBuilder.builder() + .add(KemDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()) // Use the same KEM algorithm that was used for encryption .kem("ML-KEM") // Provide the recipient’s private key for decapsulation @@ -121,7 +123,7 @@ class PostQuantumTest { // Re-derive the shared secret with the same HKDF-SHA256 label .hkdfSha256("KEM-demo".getBytes(StandardCharsets.US_ASCII)) // Use the secret to unlock AES payload decryption - .withAes(AesDataContentBuilder.builder() + .withAes(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()) // AES in GCM mode with a 128-bit tag, consistent with encryption .modeGcm(128) // Read auxiliary parameters (IV, etc.) back from the header diff --git a/samples/src/test/java/demo/SigningAesTest.java b/samples/src/test/java/demo/SigningAesTest.java index 4197f83..4a3756a 100644 --- a/samples/src/test/java/demo/SigningAesTest.java +++ b/samples/src/test/java/demo/SigningAesTest.java @@ -47,7 +47,6 @@ import javax.crypto.SecretKey; import org.junit.jupiter.api.Test; -import zeroecho.core.CryptoAlgorithms; import zeroecho.core.alg.rsa.RsaKeyGenSpec; import zeroecho.core.alg.rsa.RsaSigSpec; import zeroecho.core.tag.TagEngine; @@ -58,12 +57,15 @@ import zeroecho.sdk.builders.alg.AesDataContentBuilder; import zeroecho.sdk.builders.core.DataContentChainBuilder; import zeroecho.sdk.builders.core.PlainBytesBuilder; import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.ZeroEchoSession; class SigningAesTest { + private final ZeroEchoSession session = new ZeroEchoSession(); private static final Logger LOG = Logger.getLogger(SigningAesTest.class.getName()); KeyPair generateRsaKeys() throws GeneralSecurityException { - KeyPair kp = CryptoAlgorithms.generateKeyPair("RSA", RsaKeyGenSpec.rsa4096()); + KeyPair kp = session.keyBuilders().asymmetric() + .generateKeyPair("RSA", RsaKeyGenSpec.rsa4096()); LOG.log(Level.INFO, "RSA key public={0} private={1}", new Object[] { Strings.toShortHexString(kp.getPublic().getEncoded()), @@ -81,7 +83,7 @@ class SigningAesTest { // Configure AES in GCM mode with a 128-bit authentication tag. A fresh 256-bit // AES key will be generated automatically, and runtime parameters (IV, AAD) // will be written into the header. - AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder().generateKey(256).modeGcm(128).withHeader(); + AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).generateKey(256).modeGcm(128).withHeader(); // Generate RSA-4096 key pair (retrieved via algorithm registry for convenience) KeyPair rsa = generateRsaKeys(); @@ -89,9 +91,9 @@ class SigningAesTest { // Configure PSS signature parameters: SHA-256 hash, salt length = 32 bytes RsaSigSpec pss = RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32); // Create signing engine (RSA-PSS with private key) - TagEngine tagEnc = TagEngineBuilder.rsaSign(rsa.getPrivate(), pss).get(); + TagEngine tagEnc = TagEngineBuilder.rsaSign(new zeroecho.sdk.ZeroEchoSession(), rsa.getPrivate(), pss).get(); // Create verification engine (RSA-PSS with public key) - TagEngine tagDec = TagEngineBuilder.rsaVerify(rsa.getPublic(), pss).get(); + TagEngine tagDec = TagEngineBuilder.rsaVerify(new zeroecho.sdk.ZeroEchoSession(), rsa.getPublic(), pss).get(); // Build the encryption pipeline DataContent dccb = DataContentChainBuilder.encrypt() @@ -119,7 +121,7 @@ class SigningAesTest { .add(PlainBytesBuilder.builder().bytes(encrypted)) // AES-GCM decryption using the same key; IV and AAD are restored automatically // from the header - .add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()).modeGcm(128).withHeader()) + .add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).importKeyRaw(key.getEncoded()).modeGcm(128).withHeader()) // Verify the RSA-PSS signature trailer at the end of the stream (configured to // throw on mismatch) .add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()) @@ -142,15 +144,15 @@ class SigningAesTest { // Create a random sample message to be encrypted byte[] msg = randomBytes(100); - AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder().generateKey(256).modeGcm(128).withHeader(); + AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).generateKey(256).modeGcm(128).withHeader(); // Generate RSA-4096 key pair (retrieved via algorithm registry for convenience) KeyPair rsa = generateRsaKeys(); // Configure PSS signature parameters: SHA-256 hash, salt length = 32 bytes RsaSigSpec pss = RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32); - TagEngine tagEnc = TagEngineBuilder.rsaSign(rsa.getPrivate(), pss).get(); - TagEngine tagDec = TagEngineBuilder.rsaVerify(rsa.getPublic(), pss).get(); + TagEngine tagEnc = TagEngineBuilder.rsaSign(new zeroecho.sdk.ZeroEchoSession(), rsa.getPrivate(), pss).get(); + TagEngine tagDec = TagEngineBuilder.rsaVerify(new zeroecho.sdk.ZeroEchoSession(), rsa.getPublic(), pss).get(); // Build the encryption pipeline DataContent dccb = DataContentChainBuilder.encrypt() @@ -183,7 +185,7 @@ class SigningAesTest { .add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()) // AES-GCM decryption using the same key; IV and AAD are restored automatically // from the header - .add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()).modeGcm(128).withHeader()) + .add(AesDataContentBuilder.builder(new zeroecho.sdk.ZeroEchoSession()).importKeyRaw(key.getEncoded()).modeGcm(128).withHeader()) // Build the pipeline .build(); byte[] decrypted;