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.
This commit is contained in:
2026-07-28 19:20:30 +02:00
parent 7319aca0db
commit 49dc080c65
298 changed files with 12802 additions and 8763 deletions

View File

@@ -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.
*
* <p>
* A {@code Capability} describes one role supported by a
* {@link CryptoAlgorithm}, including:
* </p>
* <ul>
* <li>the algorithm identifier,</li>
* <li>its high-level {@link AlgorithmFamily},</li>
* <li>the {@link KeyUsage} role (e.g., ENCRYPT, VERIFY),</li>
* <li>the expected {@link CryptoContext} type,</li>
* <li>the accepted {@link Key} type,</li>
* <li>the accepted {@link ContextSpec} type, and</li>
* <li>a supplier for a default spec.</li>
* </ul>
*
* <h2>Purpose</h2> Capabilities allow discovery, inspection, and documentation
* of what an algorithm can do. Higher layers (e.g., protocol builders,
* registries, tooling) can enumerate capabilities via
* {@link CryptoAlgorithm#listCapabilities()} and adapt automatically.
*
* <p>
* Each capability corresponds to a call to
* {@link AbstractCryptoAlgorithm#capability(AlgorithmFamily, KeyUsage, Class, Class, Class, ContextConstructorKS, Supplier)}.
* </p>
*
* <h2>Thread-safety</h2> {@code Capability} instances are immutable and safe to
* share across threads.
* <p>The default specification is resolved once during provider construction.
* All components therefore have stable value semantics and are safe for
* concurrent reads.</p>
*
* @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<? extends CryptoContext> contextType, Class<? extends Key> keyType, Class<? extends ContextSpec> specType,
Supplier<? extends ContextSpec> defaultSpec) {
Class<? extends CryptoContext> contextType, Class<? extends Key> keyType,
Class<? extends ContextSpec> 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<? extends CryptoContext> contextType, Class<? extends Key> keyType,
Class<? extends ContextSpec> specType, Supplier<? extends ContextSpec> defaultSpec) {
this.algorithmId = Objects.requireNonNull(algorithmId, "algorithmId must not be null");
this.family = Objects.requireNonNull(family, "family must not be null");
this.role = Objects.requireNonNull(role, "role must not be null");
this.contextType = Objects.requireNonNull(contextType, "contextType must not be null");
this.keyType = Objects.requireNonNull(keyType, "keyType must not be null");
this.specType = Objects.requireNonNull(specType, "specType must not be null");
this.defaultSpec = Objects.requireNonNull(defaultSpec, "defaultSpec must not be null");
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());
}
}
}

View File

@@ -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.</li>
* <li>Roles: supported {@link KeyUsage} operations (e.g., ENCRYPT, SIGN) bound
* to concrete {@link CryptoContext} constructors.</li>
* <li>Key builders: factories for symmetric and asymmetric key material via
* {@link SymmetricKeyBuilder} and {@link AsymmetricKeyBuilder}.</li>
* <li>Key operations: exact generation and import capabilities.</li>
* </ul>
*
* <h2>Metadata</h2> Each algorithm instance is uniquely identified by
@@ -86,7 +82,7 @@ import zeroecho.core.spi.SymmetricKeyBuilder;
* <h2>Roles and contexts</h2> Each algorithm may support multiple
* {@link KeyUsage} roles. For each role, the algorithm binds a key type,
* context type, and optional {@link ContextSpec}. When
* {@link #create(KeyUsage, Key, ContextSpec)} is called:
* {@link #createContext(KeyUsage, Key, ContextSpec)} is called:
* <ol>
* <li>The binding for the role is located.</li>
* <li>The supplied key and spec are validated against the expected types.</li>
@@ -94,15 +90,9 @@ import zeroecho.core.spi.SymmetricKeyBuilder;
* factory.</li>
* </ol>
*
* <h2>Key builders</h2>
* <ul>
* <li>Asymmetric builders: registered via {@link #registerAsymmetricKeyBuilder}
* and accessed through {@link #asymmetricKeyBuilder(Class)} or convenience
* methods like {@link #generateKeyPair(AlgorithmKeySpec)}.</li>
* <li>Symmetric builders: registered via {@link #registerSymmetricKeyBuilder}
* and accessed through {@link #symmetricKeyBuilder(Class)} or convenience
* methods like {@link #generateSecret(AlgorithmKeySpec)}.</li>
* </ul>
* <h2>Key operations</h2> Providers register generation and import operations
* independently. Lookup returns an interface that guarantees the requested
* operation.
*
* <h2>Provider model</h2> 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;
*
* <p>
* <b>Security note:</b> 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.
* </p>
*
@@ -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<Capability> capabilities = new ArrayList<>();
private final Map<KeyUsage, List<RoleBinding<?, ?, ?>>> ctxBindings = new EnumMap<>(KeyUsage.class);
private final Map<Class<? extends AlgorithmKeySpec>, AsymEntry<?>> asymBuilders = new HashMap<>();
private final Map<Class<? extends AlgorithmKeySpec>, SymEntry<?>> symBuilders = new HashMap<>();
private final Map<Class<? extends AlgorithmKeySpec>, AsymmetricKeyPairGenerator<?>> keyPairGenerators =
new LinkedHashMap<>();
private final Map<Class<? extends AlgorithmKeySpec>, PublicKeyImporter<?>> publicKeyImporters =
new LinkedHashMap<>();
private final Map<Class<? extends AlgorithmKeySpec>, PrivateKeyImporter<?>> privateKeyImporters =
new LinkedHashMap<>();
private final Map<Class<? extends AlgorithmKeySpec>, SymmetricKeyGenerator<?>> symmetricKeyGenerators =
new LinkedHashMap<>();
private final Map<Class<? extends AlgorithmKeySpec>, SymmetricKeyImporter<?>> symmetricKeyImporters =
new LinkedHashMap<>();
private final Map<Class<? extends AlgorithmKeySpec>, AlgorithmKeySpec> asymmetricDefaults = new LinkedHashMap<>();
private final Map<Class<? extends AlgorithmKeySpec>, 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<C> ctxType;
private final Class<K> keyType;
private final Class<S> specType;
private final ContextConstructorKS<C, K, S> ctor;
private final ContextFactoryKS<C, K, S> factory;
private final Supplier<? extends S> defaultSpec;
private RoleBinding(Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextConstructorKS<C, K, S> ctor,
private RoleBinding(Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextFactoryKS<C, K, S> factory,
Supplier<? extends S> 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
* <p>
* Concrete algorithms call this during construction to declare support for
* specific roles (e.g., {@code ENCRYPT}, {@code VERIFY}). When
* {@link #create(KeyUsage, Key, ContextSpec)} is later invoked, the provided
* {@link #createContext(KeyUsage, Key, ContextSpec)} is later invoked, the provided
* {@code key} and optional {@code spec} are matched against these bindings.
* </p>
*
@@ -309,9 +312,15 @@ public abstract class CryptoAlgorithm { // NOPMD
* @param <S> spec type
* @throws NullPointerException if any class or factory argument is {@code null}
*/
protected final <C extends CryptoContext, K extends Key, S extends ContextSpec> void bind(KeyUsage role,
Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextConstructorKS<C, K, S> factory,
protected final <C extends CryptoContext, K extends Key, S extends ContextSpec> void bindContext(KeyUsage role,
Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextFactoryKS<C, K, S> factory,
Supplier<? extends S> 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 extends CryptoContext, K extends Key, S extends ContextSpec> C create(KeyUsage role, K key, S spec)
throws IOException {
public final <C extends CryptoContext, K extends Key, S extends ContextSpec> C createContext(KeyUsage role, K key,
S spec) {
return createContextInternal(role, key, spec);
}
@SuppressWarnings("unchecked")
private <C extends CryptoContext, K extends Key, S extends ContextSpec> 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<RoleBinding<?, ?, ?>> 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<C, K, S> rb = (RoleBinding<C, K, S>) rb0;
if (rb.accepts(key, spec)) {
S resolved = (spec != null) ? spec : rb.defaultSpec.get();
C ctx = rb.ctor.create(key, resolved);
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.
* <p>
* Used for discovery and documentation (e.g., tool UIs).
* </p>
*/
public static final class AsymBuilderInfo {
public final Class<? extends AlgorithmKeySpec> specType;
public final Object defaultKeySpec;
private AsymBuilderInfo(Class<? extends AlgorithmKeySpec> specType, Object defaultKeySpec) {
this.specType = specType;
this.defaultKeySpec = defaultKeySpec;
private <S extends AlgorithmKeySpec> S resolveDefault(Class<S> specType,
Supplier<? extends S> 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.
*
* <p>
* Each {@code AsymEntry} is keyed by a specific {@link AlgorithmKeySpec}
* subtype. It holds the {@link AsymmetricKeyBuilder} instance capable of
* generating or importing keys for that spec, and an optional supplier that
* provides a safe default spec (if the algorithm wants to support "generate
* with defaults").
* </p>
* <p>The optional default is resolved and validated during registration.
* Registered generators must be safe for concurrent invocation after the
* algorithm is published.</p>
*
* <h2>Usage</h2>
* <ul>
* <li>Created during calls to
* {@link #registerAsymmetricKeyBuilder(Class, AsymmetricKeyBuilder, Supplier)}.</li>
* <li>Looked up later by {@link #asymmetricKeyBuilder(Class)} and used by
* key-generation/import convenience methods such as
* {@link #generateKeyPair(AlgorithmKeySpec)}.</li>
* </ul>
*
* <h2>Thread-safety</h2> Immutable once constructed; safe to share between
* threads.
*
* @param <S> the type of {@link AlgorithmKeySpec} handled by this entry
*/
private record AsymEntry<S extends AlgorithmKeySpec>(AsymmetricKeyBuilder<S> builder,
Supplier<? extends S> defaultKeySpec) {
/**
* Creates a new binding between a key builder and its optional default spec.
*
* @throws NullPointerException if {@code builder} is {@code null}
*/
AsymEntry {
Objects.requireNonNull(builder, "builder must not be null");
}
}
/**
* Registers an asymmetric key builder for a specific spec type.
*
* <p>
* Concrete algorithms call this during construction. The {@code specType} acts
* as a key for later lookup and must be unique within this algorithm.
* </p>
*
* @param specType the spec class accepted by {@code builder}
* @param builder builder that can generate/import keys for
* {@code specType}
* @param defaultKeySpecOrNull optional supplier for a default spec (may be
* {@code null})
* @param <S> spec type
* @throws NullPointerException if {@code specType} or {@code builder} is
* @param specType exact specification class
* @param generator non-null generator
* @param defaultSpecOrNull optional default supplier, evaluated once
* @param <S> 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 <S extends AlgorithmKeySpec> void registerAsymmetricKeyBuilder(Class<S> specType,
AsymmetricKeyBuilder<S> builder, Supplier<? extends S> defaultKeySpecOrNull) {
Objects.requireNonNull(specType, "specType must not be null");
asymBuilders.put(specType, new AsymEntry<>(builder, defaultKeySpecOrNull));
protected final <S extends AlgorithmKeySpec> void registerAsymmetricKeyPairGenerator(Class<S> specType,
AsymmetricKeyPairGenerator<S> generator, Supplier<? extends S> 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 <S> 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 <S> specification type
* @throws NullPointerException if an argument is {@code null}
*/
@SuppressWarnings("unchecked")
public final <S extends AlgorithmKeySpec> AsymmetricKeyBuilder<S> asymmetricKeyBuilder(Class<S> specType) {
AsymEntry<?> e = asymBuilders.get(specType);
if (e == null) {
throw new IllegalArgumentException(_id + " has no asymmetric key builder for " + specType.getName());
}
return (AsymmetricKeyBuilder<S>) e.builder;
protected final <S extends AlgorithmKeySpec> void registerPublicKeyImporter(Class<S> specType,
PublicKeyImporter<S> 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.
*
* <p>
* The default spec value is best-effort; suppliers may throw, in which case
* {@code defaultKeySpec} is reported as {@code null}.
* </p>
*
* @return immutable list of {@link AsymBuilderInfo} descriptors
* @param specType exact specification class
* @param importer non-null importer safe for concurrent invocation
* @param <S> specification type
* @throws NullPointerException if an argument is {@code null}
*/
public final List<AsymBuilderInfo> asymmetricBuildersInfo() {
List<AsymBuilderInfo> out = new ArrayList<>();
for (Map.Entry<Class<? extends AlgorithmKeySpec>, AsymEntry<?>> e : asymBuilders.entrySet()) {
Object def = null;
if (e.getValue().defaultKeySpec != null) {
try {
def = e.getValue().defaultKeySpec.get();
} catch (Throwable t) { // NOPMD
def = null;
}
}
out.add(new AsymBuilderInfo(e.getKey(), def));
}
return Collections.unmodifiableList(out);
protected final <S extends AlgorithmKeySpec> void registerPrivateKeyImporter(Class<S> specType,
PrivateKeyImporter<S> 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.
*
* <p>
* Each {@code SymBuilderInfo} describes the specification type that a
* {@link SymmetricKeyBuilder} can handle, along with an optional default
* specification object. These descriptors are used for discovery and
* documentation purposes, for example when rendering catalog information in
* tooling or UIs.
* </p>
* <p>The optional default is resolved and validated during registration.</p>
*
* <h2>Usage</h2>
* <ul>
* <li>Produced by {@link #symmetricBuildersInfo()}.</li>
* <li>Displayed to clients for inspection and documentation, but not used
* directly in cryptographic operations.</li>
* </ul>
*
* <h2>Thread-safety</h2> Being a {@code record}, this type is immutable and
* safe to share between threads.
*
* @param specType the specification type supported by the builder
* @param defaultKeySpec an optional default key specification instance, or
* {@code null} if no default is provided
*/
public record SymBuilderInfo(Class<? extends AlgorithmKeySpec> specType, Object defaultKeySpec) {
}
/**
* Internal entry binding a registered symmetric key builder to its optional
* default key specification supplier.
*
* <p>
* Each {@code SymEntry} is keyed by a specific {@link AlgorithmKeySpec}
* subtype. It holds the {@link SymmetricKeyBuilder} instance capable of
* generating or importing keys for that spec, and a supplier that may produce a
* default spec when none is provided explicitly.
* </p>
*
* <h2>Usage</h2>
* <ul>
* <li>Created during calls to
* {@link #registerSymmetricKeyBuilder(Class, SymmetricKeyBuilder, Supplier)}.</li>
* <li>Looked up internally when methods such as
* {@link #generateSecret(AlgorithmKeySpec)} or
* {@link #importSecret(AlgorithmKeySpec)} are invoked.</li>
* </ul>
*
* <h2>Thread-safety</h2> Immutable and thread-safe by design as a
* {@code record}.
*
* @param builder the builder instance that can create or import keys;
* must not be {@code null}
* @param defaultKeySpec supplier for a default specification, or {@code null}
* if no sensible default exists
* @param <S> the type of {@link AlgorithmKeySpec} handled by this
* entry
*/
private record SymEntry<S extends AlgorithmKeySpec>(SymmetricKeyBuilder<S> builder,
Supplier<? extends S> defaultKeySpec) {
/**
* Compact constructor that enforces non-null builder.
*
* @throws NullPointerException if {@code builder} is {@code null}
*/
SymEntry {
Objects.requireNonNull(builder, "builder must not be null");
}
}
/**
* Registers a symmetric key builder for a specific spec type.
*
* @param specType the spec class accepted by {@code builder}
* @param builder builder that can generate/import keys for
* {@code specType}
* @param defaultKeySpecOrNull optional supplier for a default spec (may be
* {@code null})
* @param <S> spec type
* @throws NullPointerException if {@code specType} or {@code builder} is
* @param specType exact specification class
* @param generator non-null generator safe for concurrent invocation
* @param defaultSpecOrNull optional default supplier, evaluated once
* @param <S> 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 <S extends AlgorithmKeySpec> void registerSymmetricKeyBuilder(Class<S> specType,
SymmetricKeyBuilder<S> builder, Supplier<? extends S> defaultKeySpecOrNull) {
Objects.requireNonNull(specType, "specType must not be null");
symBuilders.put(specType, new SymEntry<>(builder, defaultKeySpecOrNull));
protected final <S extends AlgorithmKeySpec> void registerSymmetricKeyGenerator(Class<S> specType,
SymmetricKeyGenerator<S> generator, Supplier<? extends S> 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 <S> 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 <S> specification type
* @throws NullPointerException if an argument is {@code null}
*/
protected final <S extends AlgorithmKeySpec> void registerSymmetricKeyImporter(Class<S> specType,
SymmetricKeyImporter<S> 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.
*
* <p>The returned implementation may be shared and invoked concurrently.</p>
*
* @param specType exact specification class; subclasses are not matched
* @param <S> specification type
* @return registered generator
* @throws NullPointerException if {@code specType} is {@code null}
* @throws IllegalArgumentException if no generator is registered
*/
@SuppressWarnings("unchecked")
public final <S extends AlgorithmKeySpec> SymmetricKeyBuilder<S> symmetricKeyBuilder(Class<S> specType) {
SymEntry<?> e = symBuilders.get(specType);
if (e == null) {
throw new IllegalArgumentException(_id + " has no symmetric key builder for " + specType.getName());
public final <S extends AlgorithmKeySpec> AsymmetricKeyPairGenerator<S> asymmetricKeyPairGenerator(
Class<S> specType) {
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
AsymmetricKeyPairGenerator<?> generator = keyPairGenerators.get(specType);
if (generator == null) {
throw missing("asymmetric key-pair generator", specType);
}
return (SymmetricKeyBuilder<S>) e.builder;
return (AsymmetricKeyPairGenerator<S>) generator;
}
/**
* Returns metadata about all registered symmetric builders.
* Returns the public-key importer registered for an exact specification class.
*
* <p>
* The default spec value is best-effort; suppliers may throw, in which case
* {@code defaultKeySpec} is reported as {@code null}.
* </p>
* <p>The returned implementation may be shared and invoked concurrently.</p>
*
* @return immutable list of {@link SymBuilderInfo} descriptors
*/
public final List<SymBuilderInfo> symmetricBuildersInfo() {
List<SymBuilderInfo> out = new ArrayList<>();
for (Map.Entry<Class<? extends AlgorithmKeySpec>, SymEntry<?>> e : symBuilders.entrySet()) {
Object def = null;
if (e.getValue().defaultKeySpec != null) {
try {
def = e.getValue().defaultKeySpec.get();
} catch (Throwable t) { // NOPMD
def = null;
}
}
out.add(new SymBuilderInfo(e.getKey(), def));
}
return Collections.unmodifiableList(out);
}
/**
* Generates a fresh symmetric {@link SecretKey} using the registered builder
* for {@code spec}.
*
* @param spec algorithm-specific key specification (must match a registered
* symmetric builder)
* @param <S> spec type
* @return newly generated secret key
* @throws NullPointerException if {@code spec} is {@code null}
* @throws IllegalArgumentException if no symmetric builder is registered for
* {@code spec.getClass()}
* @throws GeneralSecurityException if key generation fails or parameters are
* unsupported
* @param specType exact specification class; subclasses are not matched
* @param <S> specification type
* @return registered importer
* @throws NullPointerException if {@code specType} is {@code null}
* @throws IllegalArgumentException if no importer is registered
*/
@SuppressWarnings("unchecked")
public final <S extends AlgorithmKeySpec> SecretKey generateSecret(S spec) throws GeneralSecurityException {
Objects.requireNonNull(spec, "spec must not be null");
SymmetricKeyBuilder<S> b = symmetricKeyBuilder((Class<S>) spec.getClass());
return b.generateSecret(spec);
public final <S extends AlgorithmKeySpec> PublicKeyImporter<S> publicKeyImporter(Class<S> specType) {
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
PublicKeyImporter<?> importer = publicKeyImporters.get(specType);
if (importer == null) {
throw missing("public-key importer", specType);
}
return (PublicKeyImporter<S>) 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 <S> spec type
* @return wrapped secret key validated against the spec
* @throws NullPointerException if {@code spec} is {@code null}
* @throws IllegalArgumentException if no symmetric builder is registered for
* {@code spec.getClass()}
* @throws GeneralSecurityException if the material is invalid or does not match
* the algorithm
* <p>The returned implementation may be shared and invoked concurrently.</p>
*
* @param specType exact specification class; subclasses are not matched
* @param <S> specification type
* @return registered importer
* @throws NullPointerException if {@code specType} is {@code null}
* @throws IllegalArgumentException if no importer is registered
*/
@SuppressWarnings("unchecked")
public final <S extends AlgorithmKeySpec> SecretKey importSecret(S spec) throws GeneralSecurityException {
Objects.requireNonNull(spec, "spec must not be null");
SymmetricKeyBuilder<S> b = symmetricKeyBuilder((Class<S>) spec.getClass());
return b.importSecret(spec);
public final <S extends AlgorithmKeySpec> PrivateKeyImporter<S> privateKeyImporter(Class<S> specType) {
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
PrivateKeyImporter<?> importer = privateKeyImporters.get(specType);
if (importer == null) {
throw missing("private-key importer", specType);
}
return (PrivateKeyImporter<S>) 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 <S> concrete {@link AlgorithmKeySpec} type
* @return a freshly generated key pair
* @throws GeneralSecurityException if the supplier or builder fails
*/
private <S extends AlgorithmKeySpec> KeyPair tryGenerateWithDefault(Class<? extends AlgorithmKeySpec> specType,
AsymEntry<S> entry) throws GeneralSecurityException {
if (entry.defaultKeySpec == null) {
throw new GeneralSecurityException("no default spec supplier");
}
final S spec;
try {
spec = entry.defaultKeySpec.get();
} catch (Throwable t) { // NOPMD
throw new GeneralSecurityException("defaultSpec supplier failed for " + specType.getSimpleName() + ": "
+ t.getClass().getSimpleName() + ": " + t.getMessage(), t);
}
if (spec == null) {
throw new GeneralSecurityException("defaultSpec supplier returned null for " + specType.getSimpleName());
}
// No raw types here: S is captured from entry.
return entry.builder.generateKeyPair(spec);
}
/**
* Generates a fresh {@link KeyPair} using the first asymmetric builder that
* successfully provides a default key specification.
* <p>The returned implementation may be shared and invoked concurrently.</p>
*
* <p>
* This convenience method iterates over all registered asymmetric key builders
* that declare a non-null default {@link AlgorithmKeySpec} supplier. For each,
* it attempts to obtain the default spec and generate a key pair. If a builder
* fails (e.g., the builder only supports import or rejects the parameters), the
* method records the failure and continues with the next candidate.
* </p>
*
* <h4>Example</h4> <pre>{@code
* CryptoAlgorithm algo = CryptoAlgorithms.require("Ed25519");
* KeyPair kp = algo.generateKeyPair();
* }</pre>
*
* @return a newly generated key pair using a default spec from one of the
* registered asymmetric builders
* @throws IllegalStateException if no builder declares a default spec
* supplier
* @throws GeneralSecurityException if all candidate builders fail to generate a
* key pair; the exception message details
* individual causes
*/
public final KeyPair generateKeyPair() throws GeneralSecurityException {
StringBuilder reasons = new StringBuilder(128);
boolean attempted = false;
for (Map.Entry<Class<? extends AlgorithmKeySpec>, AsymEntry<?>> e : asymBuilders.entrySet()) {
AsymEntry<?> entry = e.getValue();
if (entry.defaultKeySpec == null) {
continue;
}
attempted = true;
try {
// Wildcard capture lets the compiler infer <S> without casts.
return tryGenerateWithDefault(e.getKey(), entry);
} catch (GeneralSecurityException ex) {
reasons.append(" - ").append(e.getKey().getSimpleName()).append(": ")
.append(ex.getClass().getSimpleName()).append(": ").append(String.valueOf(ex.getMessage()))
.append('\n');
// keep trying other builders
}
}
if (!attempted) {
throw new IllegalStateException(_id + " has no default asymmetric key spec");
}
throw new GeneralSecurityException(
_id + " failed to generate a default key pair. Reasons:\n" + reasons.toString().trim());
}
/**
* Generates a fresh {@link KeyPair} using the registered asymmetric builder for
* {@code spec}.
*
* @param spec algorithm-specific key specification (must match a registered
* asymmetric builder)
* @param <S> spec type
* @return newly generated key pair
* @throws NullPointerException if {@code spec} is {@code null}
* @throws IllegalArgumentException if no asymmetric builder is registered for
* {@code spec.getClass()}
* @throws GeneralSecurityException if key generation fails or parameters are
* unsupported
* @param specType exact specification class; subclasses are not matched
* @param <S> specification type
* @return registered generator
* @throws NullPointerException if {@code specType} is {@code null}
* @throws IllegalArgumentException if no generator is registered
*/
@SuppressWarnings("unchecked")
public final <S extends AlgorithmKeySpec> KeyPair generateKeyPair(S spec) throws GeneralSecurityException {
Objects.requireNonNull(spec, "spec must not be null");
AsymmetricKeyBuilder<S> b = asymmetricKeyBuilder((Class<S>) spec.getClass());
return b.generateKeyPair(spec);
public final <S extends AlgorithmKeySpec> SymmetricKeyGenerator<S> symmetricKeyGenerator(Class<S> specType) {
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
SymmetricKeyGenerator<?> generator = symmetricKeyGenerators.get(specType);
if (generator == null) {
throw missing("symmetric-key generator", specType);
}
return (SymmetricKeyGenerator<S>) 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 <S> spec type
* @return wrapped public key validated against the spec
* @throws NullPointerException if {@code spec} is {@code null}
* @throws IllegalArgumentException if no asymmetric builder is registered for
* {@code spec.getClass()}
* @throws GeneralSecurityException if the material is invalid or does not match
* the algorithm
* <p>The returned implementation may be shared and invoked concurrently.</p>
*
* @param specType exact specification class; subclasses are not matched
* @param <S> specification type
* @return registered importer
* @throws NullPointerException if {@code specType} is {@code null}
* @throws IllegalArgumentException if no importer is registered
*/
@SuppressWarnings("unchecked")
public final <S extends AlgorithmKeySpec> PublicKey importPublic(S spec) throws GeneralSecurityException {
Objects.requireNonNull(spec, "spec must not be null");
AsymmetricKeyBuilder<S> b = asymmetricKeyBuilder((Class<S>) spec.getClass());
return b.importPublic(spec);
public final <S extends AlgorithmKeySpec> SymmetricKeyImporter<S> symmetricKeyImporter(Class<S> specType) {
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
SymmetricKeyImporter<?> importer = symmetricKeyImporters.get(specType);
if (importer == null) {
throw missing("symmetric-key importer", specType);
}
return (SymmetricKeyImporter<S>) 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 <S> spec type
* @return wrapped private key validated against the spec
* @throws NullPointerException if {@code spec} is {@code null}
* @throws IllegalArgumentException if no asymmetric builder is registered for
* {@code spec.getClass()}
* @throws GeneralSecurityException if the material is invalid or does not match
* the algorithm
* @return immutable metadata ordered by operation and specification class
*/
@SuppressWarnings("unchecked")
public final <S extends AlgorithmKeySpec> PrivateKey importPrivate(S spec) throws GeneralSecurityException {
Objects.requireNonNull(spec, "spec must not be null");
AsymmetricKeyBuilder<S> b = asymmetricKeyBuilder((Class<S>) spec.getClass());
return b.importPrivate(spec);
public final List<KeyOperationInfo> keyOperations() {
List<KeyOperationInfo> 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<KeyOperationInfo> result, KeyOperation operation,
Map<Class<? extends AlgorithmKeySpec>, ?> operations,
Map<Class<? extends AlgorithmKeySpec>, AlgorithmKeySpec> defaults) {
for (Class<? extends AlgorithmKeySpec> specType : operations.keySet()) {
result.add(new KeyOperationInfo(operation, specType, defaults.get(specType)));
}
}
}

View File

@@ -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.
*
* <p>
* {@code CryptoAlgorithms} discovers algorithms via {@link ServiceLoader} and
* exposes:
* </p>
* <ul>
* <li>a registry from canonical algorithm id to implementation,</li>
* <li>policy hooks that validate requested operations before contexts are
* created,</li>
* <li>global audit wiring (listener + wrapping mode), and</li>
* <li>convenience methods for context creation and key generation/import.</li>
* </ul>
*
* <h2>Discovery &amp; identity</h2> Implementations register themselves using
* the Java SPI for {@link CryptoAlgorithm}. If multiple providers advertise the
* same {@linkplain CryptoAlgorithm#id() id}, the registry throws at startup to
* avoid ambiguous resolution.
*
* <h2>Policy</h2> The active {@link CryptoPolicy} is consulted before any
* context is created. Policies can deny weak parameters, enforce key-usage
* separation, or restrict algorithms. If {@link #setPolicy(CryptoPolicy)} is
* never called or is set to {@code null}, a permissive policy is used.
*
* <h2>Auditing</h2> All key lifecycle events and context creation can be
* reported to a global {@link AuditListener}. The {@link AuditMode} determines
* whether contexts are wrapped with auditing proxies or relied upon to emit
* events directly.
*
* <h2>Thread-safety</h2> The registry map and global hooks are safe to read
* concurrently. Hooks are backed by {@code volatile} fields and can be swapped
* at runtime; there is no global lock.
* <p>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.</p>
*
* @since 1.0
*/
public final class CryptoAlgorithms {
private static final Map<String, CryptoAlgorithm> BY_ID;
private static volatile CryptoPolicy<ContextSpec, Key> POLICY = CryptoPolicy.permissive(); // NOPMD
private static volatile AuditListener AUDIT = AuditListener.noop(); // NOPMD
private static volatile AuditMode AUDIT_MODE = AuditMode.OFF; // NOPMD
private static final Map<String, CryptoAlgorithm> BY_ID = loadRegistry();
private CryptoAlgorithms() {
}
static {
Map<String, CryptoAlgorithm> m = new HashMap<>();
for (CryptoAlgorithm a : ServiceLoader.load(CryptoAlgorithm.class)) {
CryptoAlgorithm prev = m.put(a.id(), a);
if (prev != null) {
throw new IllegalStateException("Duplicate algorithm id: " + a.id());
private static Map<String, CryptoAlgorithm> loadRegistry() {
Map<String, CryptoAlgorithm> 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<String, CryptoAlgorithm> registry() {
return BY_ID;
}
/**
* Returns the set of available algorithm identifiers discovered via
* {@link ServiceLoader}.
* Returns registered algorithm identifiers in deterministic order.
*
* <p>
* The returned set is backed by an unmodifiable registry snapshot. Use these
* identifiers with {@link #require(String)} or the convenience methods below.
* </p>
*
* @return unmodifiable set of canonical algorithm ids
* @return unmodifiable set of canonical identifiers
*/
public static Set<String> available() {
return BY_ID.keySet();
}
/**
* Looks up an algorithm implementation by its canonical identifier.
* Resolves an algorithm by canonical identifier.
*
* <p>
* If the id is unknown, an {@link IllegalArgumentException} is thrown. This
* method is preferred over direct access to ensure consistent error handling
* and to centralize future selection logic.
* </p>
*
* @param id canonical algorithm identifier (e.g., {@code "AES/GCM"} or
* {@code "Ed25519"})
* @return the corresponding {@link CryptoAlgorithm} implementation
* @throws IllegalArgumentException if no algorithm is registered under
* @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.
*
* <p>
* Pass {@code null} to revert to a permissive policy. Policies should be fast
* and side-effect free; they are invoked on every
* {@link #create(String, KeyUsage, Key, ContextSpec)} call.
* </p>
*
* @param p policy to install, or {@code null} to use
* {@link CryptoPolicy#permissive()}
*/
public static void setPolicy(CryptoPolicy<ContextSpec, Key> p) {
POLICY = (p == null ? CryptoPolicy.<ContextSpec, Key>permissive() : p);
}
/**
* Sets the global {@link AuditListener}.
*
* <p>
* Pass {@code null} to disable custom auditing (a no-op listener will be
* installed). The listener may be invoked by context proxies (in
* {@link AuditMode#WRAP}) and by the convenience key factory methods below.
* </p>
*
* @param l listener instance or {@code null} for a no-op listener
*/
public static void setAuditListener(AuditListener l) {
AUDIT = (l == null ? AuditListener.noop() : l);
}
/**
* Returns the current global {@link AuditListener}.
*
* @return the active audit listener (never {@code null})
*/
public static AuditListener audit() {
return AUDIT;
}
/**
* Declares how auditing is applied to cryptographic contexts.
*
* <p>
* The {@code AuditMode} controls whether contexts created by
* {@link CryptoAlgorithms#create(String, KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}
* are wrapped in auditing proxies or whether auditing is delegated entirely to
* the caller.
* </p>
*
* <h2>Modes</h2>
* <ul>
* <li>{@link #OFF} - No automatic wrapping of contexts (default). Only explicit
* events triggered at creation are emitted; no per-operation auditing is
* injected.</li>
*
* <li>{@link #WRAP} - Supported contexts are wrapped in dynamic proxies that
* emit additional stream-level and per-operation auditing events. Creation
* events originate from the proxy rather than the factory method.</li>
*
* <li>{@link #MANUAL} - No automatic wrapping and no automatic event emission.
* The caller is fully responsible for invoking audit methods (e.g.,
* {@link CryptoAlgorithms#audit()}) at the appropriate times.</li>
* </ul>
*
* @since 1.0
*/
public enum AuditMode {
/**
* No automatic wrapping of contexts (default).
*
* <p>
* Only explicit events emitted here (e.g.,
* {@link AuditListener#onContextCreated}) are sent to the listener;
* stream-level or per-operation auditing is not injected.
* </p>
*/
OFF,
/**
* Wraps supported contexts in dynamic proxies that emit stream-level auditing.
*
* <p>
* In this mode, creation events are emitted by the proxy rather than here, and
* subsequent operations (e.g., updates, finalization) may also be audited
* depending on the proxy implementation.
* </p>
*/
WRAP,
/**
* No wrapping and no automatic events.
*
* <p>
* The caller is responsible for emitting all relevant audit events via the
* {@link #audit()} listener.
* </p>
*/
MANUAL
}
/**
* Sets the auditing mode for subsequently created contexts.
*
* <p>
* Passing {@code null} resets the mode to {@link AuditMode#OFF}.
* </p>
*
* @param mode desired auditing strategy or {@code null} for {@code OFF}
*/
public static void setAuditMode(AuditMode mode) {
AUDIT_MODE = (mode == null ? AuditMode.OFF : mode);
}
/**
* Returns the current auditing mode.
*
* @return active {@link AuditMode}; never {@code null}
*/
public static AuditMode getAuditMode() {
return AUDIT_MODE;
}
/**
* Creates a {@link CryptoContext} for the given algorithm id and role, applying
* policy validation and optional auditing/wrapping.
*
* <p>
* Flow:
* </p>
* <ol>
* <li>Policy validation via
* {@link CryptoPolicy#validate(String, KeyUsage, Key, ContextSpec)}.</li>
* <li>Algorithm resolution via {@link #require(String)} and context
* construction via
* {@link CryptoAlgorithm#create(KeyUsage, Key, ContextSpec)}.</li>
* <li>Auditing behavior based on {@link #getAuditMode()}:
* <ul>
* <li>{@link AuditMode#OFF}/{@link AuditMode#MANUAL}: emit a creation event
* immediately via
* {@link AuditListener#onContextCreated(String, String, KeyUsage, Key, ContextSpec)}.</li>
* <li>{@link AuditMode#WRAP}: return a proxy (where supported) that emits
* creation and stream-level events; unknown context types are returned
* unwrapped.</li>
* </ul>
* </li>
* </ol>
*
* @param id canonical algorithm identifier
* @param role desired {@link KeyUsage} (e.g., ENCRYPT, VERIFY)
* @param key key instance for the role
* @param spec optional context specification; may be {@code null} to use
* algorithm defaults
* @param <C> context type
* @param <K> key type
* @param <S> spec type
* @return a context ready for use; may be a proxy if {@link AuditMode#WRAP} is
* active
* @throws IOException if the underlying algorithm fails to create
* a context
* @throws IllegalArgumentException if {@code id} is unknown
* @throws UnsupportedRoleException if the algorithm does not support
* {@code role}
* @throws UnsupportedSpecException if the provided key/spec are incompatible
* with the role
*/
public static <C extends CryptoContext, K extends Key, S extends ContextSpec> C create(String id, KeyUsage role,
K key, S spec) throws IOException {
POLICY.validate(id, role, key, spec);
CryptoAlgorithm algo = require(id);
C ctx = algo.create(role, key, spec);
// In WRAP mode, the proxy will emit creation metadata/events.
if (AUDIT_MODE != AuditMode.WRAP) {
AUDIT.onContextCreated(algo.id(), algo.providerName(), role, key, spec);
}
if (AUDIT_MODE == AuditMode.WRAP) {
final AuditListener listener = AUDIT; // pass through the global listener
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.
*
* <p>
* The returned context remains owned by the caller of the factory method. This
* helper does not acquire an additional resource requiring local cleanup.
* </p>
*
* @param <C> context type
* @param context source context
* @param listener audit listener
* @param role key usage role
* @return audited wrapper
*/
@SuppressWarnings("unchecked")
/* default */ static <C extends CryptoContext> C wrapForAudit(CryptoContext context, AuditListener listener,
KeyUsage role) {
return (C) AuditedContexts.wrap(context, listener, role);
}
/**
* Creates a {@link CryptoContext} using the algorithms default spec for the
* role.
*
* <p>
* Equivalent to {@code create(id, role, key, null)}.
* </p>
*
* @param id canonical algorithm identifier
* @param role desired {@link KeyUsage}
* @param key key instance for the role
* @param <C> context type
* @param <K> key type
* @return a context ready for use
* @throws IOException if the underlying algorithm fails to create
* a context
* @throws IllegalArgumentException if {@code id} is unknown
* @throws UnsupportedRoleException if the algorithm does not support
* {@code role}
*/
public static <C extends CryptoContext, K extends Key> C create(String id, KeyUsage role, K key)
throws IOException {
return create(id, role, key, null);
}
/**
* Generates a fresh asymmetric {@link KeyPair} for the given algorithm id and
* spec.
*
* <p>
* Emits
* {@link AuditListener#onKeyGenerated(String, String, AlgorithmKeySpec, KeyPair)}
* on success.
* </p>
*
* @param id canonical algorithm identifier
* @param spec algorithm-specific key specification
* @param <S> spec type
* @return newly generated key pair
* @throws GeneralSecurityException if key generation fails
* @throws IllegalArgumentException if {@code id} is unknown or the spec is
* unsupported
*/
public static <S extends AlgorithmKeySpec> KeyPair keyPair(String id, S spec) throws GeneralSecurityException {
CryptoAlgorithm algo = require(id);
@SuppressWarnings("unchecked")
KeyPair kp = algo.asymmetricKeyBuilder((Class<S>) spec.getClass()).generateKeyPair(spec);
AUDIT.onKeyGenerated(algo.id(), algo.providerName(), spec, kp);
return kp;
}
/**
* Imports a {@link PublicKey} using the algorithms registered asymmetric
* builder.
*
* <p>
* Emits {@link AuditListener#onKeyBuilt(String, String, AlgorithmKeySpec, Key)}
* on success.
* </p>
*
* @param id canonical algorithm identifier
* @param spec algorithm-specific key specification containing encoded public
* material
* @param <S> spec type
* @return imported public key
* @throws GeneralSecurityException if import fails or material is invalid
* @throws IllegalArgumentException if {@code id} is unknown or the spec is
* unsupported
*/
public static <S extends AlgorithmKeySpec> PublicKey publicKey(String id, S spec) throws GeneralSecurityException {
CryptoAlgorithm algo = require(id);
@SuppressWarnings("unchecked")
PublicKey k = algo.asymmetricKeyBuilder((Class<S>) spec.getClass()).importPublic(spec);
AUDIT.onKeyBuilt(algo.id(), algo.providerName(), spec, k);
return k;
}
/**
* Imports a {@link PrivateKey} using the algorithms registered asymmetric
* builder.
*
* <p>
* Emits {@link AuditListener#onKeyBuilt(String, String, AlgorithmKeySpec, Key)}
* on success.
* </p>
*
* @param id canonical algorithm identifier
* @param spec algorithm-specific key specification containing encoded private
* material
* @param <S> spec type
* @return imported private key
* @throws GeneralSecurityException if import fails or material is invalid
* @throws IllegalArgumentException if {@code id} is unknown or the spec is
* unsupported
*/
public static <S extends AlgorithmKeySpec> PrivateKey privateKey(String id, S spec)
throws GeneralSecurityException {
CryptoAlgorithm algo = require(id);
@SuppressWarnings("unchecked")
PrivateKey k = algo.asymmetricKeyBuilder((Class<S>) spec.getClass()).importPrivate(spec);
AUDIT.onKeyBuilt(algo.id(), algo.providerName(), spec, k);
return k;
}
/**
* Imports a symmetric {@link SecretKey} using the algorithms registered
* builder.
*
* <p>
* Emits {@link AuditListener#onKeyBuilt(String, String, AlgorithmKeySpec, Key)}
* on success.
* </p>
*
* @param id canonical algorithm identifier
* @param spec algorithm-specific key specification containing raw/encoded
* material
* @param <S> spec type
* @return imported secret key
* @throws GeneralSecurityException if import fails or material is invalid
* @throws IllegalArgumentException if {@code id} is unknown or the spec is
* unsupported
*/
public static <S extends AlgorithmKeySpec> SecretKey secretKey(String id, S spec) throws GeneralSecurityException {
CryptoAlgorithm algo = require(id);
@SuppressWarnings("unchecked")
SecretKey k = algo.symmetricKeyBuilder((Class<S>) spec.getClass()).importSecret(spec);
AUDIT.onKeyBuilt(algo.id(), algo.providerName(), spec, k);
return k;
}
/**
* Attempts to destroy a key via the JDK {@code Destroyable} interface.
*
* <p>
* If destruction succeeds,
* {@link AuditListener#onKeyDestroyed(String, String, Key)} is emitted. Any
* exceptions from {@code destroy()} are swallowed; the method returns
* {@code false} when destruction did not occur.
* </p>
*
* @param algoId algorithm identifier used for audit metadata
* @param provider provider name used for audit metadata
* @param key key to destroy
* @return {@code true} if the key reported destroyed, {@code false} otherwise
*/
public static boolean destroyKey(String algoId, String provider, Key key) {
boolean destroyed = false;
try {
if (key instanceof javax.security.auth.Destroyable) {
javax.security.auth.Destroyable d = (javax.security.auth.Destroyable) key;
if (!d.isDestroyed()) {
d.destroy();
destroyed = true;
}
}
} catch (Exception ignored) {
// swallow and report via audit only if destroyed
}
if (destroyed) {
AUDIT.onKeyDestroyed(algoId, provider, key);
}
return destroyed;
}
/**
* Convenience wrapper for
* {@link CryptoAlgorithm#generateSecret(AlgorithmKeySpec)}.
*
* @param id canonical algorithm identifier
* @param spec algorithm-specific key specification
* @param <S> spec type
* @return newly generated secret key
* @throws GeneralSecurityException if key generation fails
* @throws IllegalArgumentException if {@code id} is unknown
*/
public static <S extends AlgorithmKeySpec> SecretKey generateSecret(String id, S spec)
throws GeneralSecurityException {
return require(id).generateSecret(spec);
}
/**
* Convenience wrapper for
* {@link CryptoAlgorithm#generateKeyPair(AlgorithmKeySpec)}.
*
* @param id canonical algorithm identifier
* @param spec algorithm-specific key specification
* @param <S> spec type
* @return newly generated key pair
* @throws GeneralSecurityException if key generation fails
* @throws IllegalArgumentException if {@code id} is unknown
*/
public static <S extends AlgorithmKeySpec> KeyPair generateKeyPair(String id, S spec)
throws GeneralSecurityException {
return require(id).generateKeyPair(spec);
}
/**
* Convenience wrapper for
* {@link CryptoAlgorithm#importPublic(AlgorithmKeySpec)}.
*
* @param id canonical algorithm identifier
* @param spec algorithm-specific key specification
* @param <S> spec type
* @return imported public key
* @throws GeneralSecurityException if import fails
* @throws IllegalArgumentException if {@code id} is unknown
*/
public static <S extends AlgorithmKeySpec> PublicKey importPublic(String id, S spec)
throws GeneralSecurityException {
return require(id).importPublic(spec);
}
/**
* Convenience wrapper for
* {@link CryptoAlgorithm#importPrivate(AlgorithmKeySpec)}.
*
* @param id canonical algorithm identifier
* @param spec algorithm-specific key specification
* @param <S> spec type
* @return imported private key
* @throws GeneralSecurityException if import fails
* @throws IllegalArgumentException if {@code id} is unknown
*/
public static <S extends AlgorithmKeySpec> PrivateKey importPrivate(String id, S spec)
throws GeneralSecurityException {
return require(id).importPrivate(spec);
}
/**
* Convenience wrapper for
* {@link CryptoAlgorithm#importSecret(AlgorithmKeySpec)}.
*
* @param id canonical algorithm identifier
* @param spec algorithm-specific key specification
* @param <S> spec type
* @return imported secret key
* @throws GeneralSecurityException if import fails
* @throws IllegalArgumentException if {@code id} is unknown
*/
public static <S extends AlgorithmKeySpec> SecretKey importSecret(String id, S spec)
throws GeneralSecurityException {
return require(id).importSecret(spec);
return algorithm;
}
}

View File

@@ -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;
*
* <p>
* {@code CryptoCatalog} is a lightweight registry built at a point in time via
* {@link #load()}. It collects algorithms published through the Java SPI for
* {@link CryptoAlgorithm}, ensures identifier uniqueness, and exposes:
* {@link #load()}. It consumes the authoritative provider registry owned by
* {@link CryptoAlgorithms} and exposes:
* </p>
*
* <ul>
@@ -60,8 +57,8 @@ import zeroecho.core.annotation.DisplayName;
* </ul>
*
* <h2>Identity and uniqueness</h2> Algorithm ids are treated as canonical keys.
* If two providers expose the same {@linkplain CryptoAlgorithm#id() id}, the
* catalog build fails with {@link IllegalStateException}.
* Duplicate provider identifiers are rejected when the authoritative registry
* is initialized.
*
* <h2>Immutability &amp; thread-safety</h2> 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;
* </ul>
*
* <p>
* <b>Note:</b> Default spec / key-spec values shown in outputs are derived from
* {@code Supplier}s registered by algorithms. Suppliers may compute labels or
* return lightweight descriptors; their intent is documentation, not
* roundtripping.
* <b>Note:</b> Default spec / key-spec values shown in outputs are stable
* metadata values resolved when providers are initialized; their intent is
* documentation, not round-tripping.
* </p>
*
* @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}.
*
* <p>
* 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.
* </p>
*
* @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<String, CryptoAlgorithm> m = new HashMap<>();
ServiceLoader.load(CryptoAlgorithm.class).forEach(a -> {
CryptoAlgorithm prev = m.put(a.id(), a);
if (prev != null) {
throw new IllegalStateException("Duplicate algorithm id: " + a.id());
}
});
return new CryptoCatalog(Collections.unmodifiableMap(m));
return new CryptoCatalog(CryptoAlgorithms.registry());
}
/* default */ Map<String, CryptoAlgorithm> 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("</contextType><keyType>")
.append(esc(cap.keyType().getSimpleName())).append("</keyType><specType>")
.append(esc(cap.specType().getSimpleName())).append("</specType><defaultSpec>")
.append(esc(labelOf(cap.defaultSpec().get()))).append("</defaultSpec></capability>");
.append(esc(labelOf(cap.defaultSpec()))).append("</defaultSpec></capability>");
}
sb.append("</capabilities><asymmetricKeyBuilders>");
for (CryptoAlgorithm.AsymBuilderInfo kb : a.asymmetricBuildersInfo()) {
sb.append("<keyBuilder specType=\"").append(esc(kb.specType.getSimpleName()))
.append("\"><defaultKeySpec>")
.append(kb.defaultKeySpec == null ? "" : esc(labelOf(kb.defaultKeySpec)))
.append("</defaultKeySpec></keyBuilder>");
sb.append("</capabilities><keyOperations>");
for (KeyOperationInfo operation : a.keyOperations()) {
sb.append("<keyOperation operation=\"").append(operation.operation().name())
.append("\" specType=\"").append(esc(operation.specType().getSimpleName()))
.append("\"><defaultSpec>")
.append(operation.defaultSpec() == null ? "" : esc(labelOf(operation.defaultSpec())))
.append("</defaultSpec></keyOperation>");
}
sb.append("</asymmetricKeyBuilders><symmetricKeyBuilders>");
for (CryptoAlgorithm.SymBuilderInfo kb : a.symmetricBuildersInfo()) {
sb.append("<keyBuilder specType=\"").append(esc(kb.specType().getSimpleName()))
.append("\"><defaultKeySpec>")
.append(kb.defaultKeySpec() == null ? "" : esc(labelOf(kb.defaultKeySpec())))
.append("</defaultKeySpec></keyBuilder>");
}
sb.append("</symmetricKeyBuilders></algorithm>");
sb.append("</keyOperations></algorithm>");
}
sb.append("</cryptoCatalog>");
return sb.toString();

View File

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

View File

@@ -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<? extends AlgorithmKeySpec> 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");
}
}
}

View File

@@ -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;
*
* <ol>
* <li><b>Binding roles to runtime factories</b> 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 {
* </p>
* <ul>
* <li><b>Runtime binding:</b> delegates to
* {@link CryptoAlgorithm#bind(KeyUsage, Class, Class, Class, ContextConstructorKS, Supplier)}
* so that {@link CryptoAlgorithm#create(KeyUsage, Key, ContextSpec)} can
* {@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.</li>
* <li><b>Metadata publication:</b> 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 {
* </ul>
*
* <h4>Validation</h4> 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 <C> context type
* @param <K> key type
* @param <S> 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 <C extends CryptoContext, K extends Key, S extends ContextSpec> void capability(AlgorithmFamily family,
KeyUsage role, Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextConstructorKS<C, K, S> factory,
KeyUsage role, Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextFactoryKS<C, K, S> factory,
Supplier<? extends S> 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));
}
}

View File

@@ -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);
});
}
}

View File

@@ -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());
}
/**

View File

@@ -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;
* <p>
* This class wraps raw key material (16, 24, or 32 bytes) for use with the AES
* algorithm. Factory methods support construction from raw bytes, hex strings,
* or Base64-encoded strings. The key material is defensively copied to maintain
* immutability.
* or Base64-encoded strings. The key material is defensively copied.
* </p>
*
* <p>
@@ -58,13 +60,16 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* </p>
*
* <p>
* 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.
* </p>
*
* @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);
}
}

View File

@@ -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;
/**
* <h2>Integration of BIKE (Bit Flipping Key Encapsulation) algorithm</h2>
@@ -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);
* }</pre>
*
* @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);
});
}
/**

View File

@@ -44,7 +44,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* <h3>Usage</h3> <pre>{@code
* // Generate a BIKE-192 key pair
* KeyPair kp = bikeAlgorithm.asymmetricKeyBuilder(BikeKeyGenSpec.class)
* KeyPair kp = bikeAlgorithm.asymmetricKeyPairGenerator(BikeKeyGenSpec.class)
* .generateKeyPair(BikeKeyGenSpec.bike192());
* }</pre>
*

View File

@@ -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;
* <h3>Usage</h3> <pre>{@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");
}
}
}

View File

@@ -49,7 +49,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <h3>Usage</h3> <pre>{@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);

View File

@@ -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;
/**
* <h2>Abstract base for ChaCha family algorithms</h2>
@@ -64,19 +66,20 @@ import zeroecho.core.spi.SymmetricKeyBuilder;
* {@code "ChaCha20"}.</li>
* <li>Import wraps the raw key material with
* {@link javax.crypto.spec.SecretKeySpec}.</li>
* <li>Attempts to generate a key via {@code ChaChaKeyImportSpec} or import via
* {@code ChaChaKeyGenSpec} will throw
* {@link UnsupportedOperationException}.</li>
* <li>Generation and import are discovered through independent exact
* capabilities, so an unsupported lookup fails before invocation.</li>
* </ul>
*
* <h3>Example</h3> <pre>{@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));
* }</pre>
*
* @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);
});
}
}

View File

@@ -74,19 +74,19 @@ import zeroecho.core.SymmetricHeaderCodec;
* corresponding cipher context.
*
* <h3>Example</h3> <pre>{@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);
* }</pre>
*
* @since 1.0

View File

@@ -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;
* <h3>Usage</h3> <pre>{@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);
}
}

View File

@@ -37,9 +37,10 @@
* <p>
* 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.
* </p>

View File

@@ -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;
/**
* <h2>Classic McEliece (CMCE) algorithm adapter</h2>
@@ -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);
* }</pre>
*
* @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 {

View File

@@ -52,7 +52,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <pre>{@code
* // Generate a key pair for McEliece 8192128F (256-bit security, fast)
* CmceKeyGenSpec spec = CmceKeyGenSpec.mceliece8192128f();
* KeyPair kp = alg.asymmetricKeyBuilder(CmceKeyGenSpec.class).generateKeyPair(spec);
* KeyPair kp = alg.asymmetricKeyPairGenerator(CmceKeyGenSpec.class).generateKeyPair(spec);
* }</pre>
*
* @since 1.0

View File

@@ -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;
* </p>
*
* <p>
* 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.
* </p>
*
* <h2>Marshalling</h2>
@@ -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");
}
}
}

View File

@@ -67,8 +67,8 @@
* selects a CMCE parameter set (variant) used by the key-pair builder.</li>
* <li><b>Key import specs:</b> {@link zeroecho.core.alg.cmce.CmcePublicKeySpec}
* wraps X.509 public keys and {@link zeroecho.core.alg.cmce.CmcePrivateKeySpec}
* wraps PKCS#8 private keys; both are immutable and defensively copy their byte
* arrays.</li>
* wraps PKCS#8 private keys; both defensively copy their byte arrays, and the
* private-key form is destroyable.</li>
* </ul>
*
* <h2>Provider requirements</h2>

View File

@@ -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;
/**
* <h2>Generic JCA-based Key Agreement Context</h2>
*
* An {@link AgreementContext} backed by the standard JCA {@link KeyAgreement}
* API. This class supports elliptic-curve and modern Diffie-Hellman variants
* 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.
*
* <p>
* 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.
* </p>
*
@@ -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();
}
/**

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
/**
* <h2>Abstract EdDSA Key-Pair Builder</h2>
@@ -71,7 +69,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
*
* @since 1.0
*/
public abstract class AbstractEdDSAKeyGenBuilder<S extends AlgorithmKeySpec> implements AsymmetricKeyBuilder<S> {
public abstract class AbstractEdDSAKeyGenBuilder<S extends AlgorithmKeySpec> implements AsymmetricKeyPairGenerator<S> {
/**
* Returns the JCA algorithm name understood by {@link KeyPairGenerator}.
*
@@ -92,9 +90,8 @@ public abstract class AbstractEdDSAKeyGenBuilder<S extends AlgorithmKeySpec> imp
* Generates a new EdDSA key pair using JCA defaults.
*
* <p>
* The provided {@code spec} is not inspected in this base implementation, but
* it satisfies the {@link AsymmetricKeyBuilder} contract. Subclasses may extend
* this behavior to interpret spec parameters.
* The provided {@code spec} is not inspected in this base implementation.
* Subclasses may extend this behavior to interpret specification parameters.
* </p>
*
* @param spec algorithm-specific key specification (currently unused)
@@ -107,38 +104,4 @@ public abstract class AbstractEdDSAKeyGenBuilder<S extends AlgorithmKeySpec> imp
KeyPairGenerator kpg = KeyPairGenerator.getInstance(jcaKeyPairAlg());
return kpg.generateKeyPair();
}
/**
* Always throws, as this builder does not support public key import.
*
* <p>
* Importing encoded EdDSA public keys must be done through the corresponding
* {@code *PublicKeySpec} builder class.
* </p>
*
* @param spec algorithm-specific key specification
* @return never returns normally
* @throws UnsupportedOperationException always thrown
*/
@Override
public PublicKey importPublic(S spec) {
throw new UnsupportedOperationException("Use the corresponding PublicKeySpec to import a public key.");
}
/**
* Always throws, as this builder does not support private key import.
*
* <p>
* Importing encoded EdDSA private keys must be done through the corresponding
* {@code *PrivateKeySpec} builder class.
* </p>
*
* @param spec algorithm-specific key specification
* @return never returns normally
* @throws UnsupportedOperationException always thrown
*/
@Override
public PrivateKey importPrivate(S spec) {
throw new UnsupportedOperationException("Use the corresponding PrivateKeySpec to import a private key.");
}
}

View File

@@ -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;
/**
* <h2>Abstract EdDSA Encoded Private Key Builder</h2>
@@ -74,7 +73,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
*
* @since 1.0
*/
public abstract class AbstractEncodedPrivateKeyBuilder<S extends AlgorithmKeySpec> implements AsymmetricKeyBuilder<S> {
public abstract class AbstractEncodedPrivateKeyBuilder<S extends AlgorithmKeySpec> implements PrivateKeyImporter<S> {
/**
* Returns the canonical JCA algorithm identifier used by
* {@link KeyFactory#getInstance(String)}.
@@ -101,32 +100,6 @@ public abstract class AbstractEncodedPrivateKeyBuilder<S extends AlgorithmKeySpe
*/
protected abstract byte[] encodedPkcs8(S spec);
/**
* Unsupported in this builder, since generation is handled by the
* {@link AbstractEdDSAKeyGenBuilder}.
*
* @param spec algorithm-specific key specification
* @return never returns normally
* @throws UnsupportedOperationException always thrown
*/
@Override
public KeyPair generateKeyPair(S spec) {
throw new UnsupportedOperationException("Generation not supported by this spec.");
}
/**
* Unsupported in this builder, since public key import is delegated to the
* matching {@code *PublicKeySpec} builder.
*
* @param spec algorithm-specific key specification
* @return never returns normally
* @throws UnsupportedOperationException always thrown
*/
@Override
public PublicKey importPublic(S spec) {
throw new UnsupportedOperationException("Use the corresponding PublicKeySpec.");
}
/**
* Imports an EdDSA private key from its PKCS#8-encoded form.
*
@@ -144,6 +117,11 @@ public abstract class AbstractEncodedPrivateKeyBuilder<S extends AlgorithmKeySpe
@Override
public PrivateKey importPrivate(S spec) throws GeneralSecurityException {
KeyFactory kf = KeyFactory.getInstance(jcaKeyFactoryAlg());
return kf.generatePrivate(new PKCS8EncodedKeySpec(encodedPkcs8(spec)));
byte[] encoded = encodedPkcs8(spec);
try {
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
} finally {
Arrays.fill(encoded, (byte) 0);
}
}
}

View File

@@ -35,13 +35,11 @@ package zeroecho.core.alg.common.eddsa;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import zeroecho.core.spec.AlgorithmKeySpec;
import zeroecho.core.spi.AsymmetricKeyBuilder;
import zeroecho.core.spi.PublicKeyImporter;
/**
* <h2>Abstract EdDSA Encoded Public Key Builder</h2>
@@ -75,7 +73,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
*
* @since 1.0
*/
public abstract class AbstractEncodedPublicKeyBuilder<S extends AlgorithmKeySpec> implements AsymmetricKeyBuilder<S> {
public abstract class AbstractEncodedPublicKeyBuilder<S extends AlgorithmKeySpec> implements PublicKeyImporter<S> {
/**
* Returns the canonical JCA algorithm identifier used by
@@ -103,19 +101,6 @@ public abstract class AbstractEncodedPublicKeyBuilder<S extends AlgorithmKeySpec
*/
protected abstract byte[] encodedX509(S spec);
/**
* Unsupported in this builder, since key pair generation is handled by
* {@link AbstractEdDSAKeyGenBuilder}.
*
* @param spec algorithm-specific key specification
* @return never returns normally
* @throws UnsupportedOperationException always thrown
*/
@Override
public KeyPair generateKeyPair(S spec) {
throw new UnsupportedOperationException("Generation not supported by this spec.");
}
/**
* Imports an EdDSA public key from its X.509-encoded form.
*
@@ -135,17 +120,4 @@ public abstract class AbstractEncodedPublicKeyBuilder<S extends AlgorithmKeySpec
KeyFactory kf = KeyFactory.getInstance(jcaKeyFactoryAlg());
return kf.generatePublic(new X509EncodedKeySpec(encodedX509(spec)));
}
/**
* Unsupported in this builder, since private key import is delegated to the
* matching {@code *PrivateKeySpec} builder.
*
* @param spec algorithm-specific key specification
* @return never returns normally
* @throws UnsupportedOperationException always thrown
*/
@Override
public PrivateKey importPrivate(S spec) {
throw new UnsupportedOperationException("Use the corresponding PrivateKeySpec.");
}
}

View File

@@ -145,7 +145,7 @@ public final class GenericJcaSignatureContext implements SignatureContext {
// lifecycle
private boolean wrapped; // = false;
private Stream activeStream;
private SignatureStream activeStream;
private boolean autoCloseActiveStream;
/**
@@ -449,7 +449,7 @@ public final class GenericJcaSignatureContext implements SignatureContext {
LOG.log(Level.INFO, "wrap for signing, tagLength={0}", declaredTagLen);
Stream s = new Stream(engine, signMode, upstream, tagLength(), expectedTag, verifier());
SignatureStream s = new SignatureStream(engine, signMode, upstream, tagLength(), expectedTag, verifier());
this.activeStream = s;
return s;
}

View File

@@ -116,7 +116,7 @@ public final class SignatureInteropProfile { // NOPMD
* @param keyAlgorithmId ZeroEcho key algorithm identifier used for key
* import and matching, such as {@code RSA}
* @param contextAlgorithmId ZeroEcho context algorithm identifier used
* with {@code CryptoAlgorithms.create(...)}
* with {@code ZeroEchoSession.createContext(...)}
* @param contextSpec explicit ZeroEcho context specification
* @param signatureRepresentation signature representation bridge between
* external bytes and internal ZeroEcho bytes

View File

@@ -81,8 +81,8 @@ import zeroecho.core.util.Strings;
* upstream stream.
* </p>
*/
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<Signature> strategy) {
super(upstream, bodyBufSize);
this.engine = engine;

View File

@@ -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.</li>
* <li><b>Stream</b> - internal passthrough input stream that feeds chunks to
* <li><b>SignatureStream</b> - internal passthrough input stream that feeds chunks to
* the signature engine, emits the trailer in SIGN mode, and performs final
* verification in VERIFY mode.</li>
* </ul>

View File

@@ -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);
});
}
}

View File

@@ -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;
/**
* <h2>DH key pair builder</h2>
@@ -89,7 +87,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* KeyPair kp2 = builder.generateKeyPair(sized);
* }</pre>
*/
public final class DhKeyGenBuilder implements AsymmetricKeyBuilder<DhSpec> {
public final class DhKeyGenBuilder implements AsymmetricKeyPairGenerator<DhSpec> {
/**
* Generates a Diffie-Hellman key pair for the given specification.
*
@@ -138,56 +136,4 @@ public final class DhKeyGenBuilder implements AsymmetricKeyBuilder<DhSpec> {
kpg.initialize(dh);
return kpg.generateKeyPair();
}
/**
* Unsupported for DH in this builder.
*
* <p>
* 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.
* </p>
*
* <p>
* <strong>Example</strong>
* </p>
* <pre>{@code
* // This will throw UnsupportedOperationException
* new DhKeyGenBuilder().importPublic(DhSpec.ffdhe2048());
* }</pre>
*
* @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.
*
* <p>
* 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.
* </p>
*
* <p>
* <strong>Example</strong>
* </p>
* <pre>{@code
* // This will throw UnsupportedOperationException
* new DhKeyGenBuilder().importPrivate(DhSpec.ffdhe2048());
* }</pre>
*
* @param spec the DH specification (ignored)
* @return never returns normally
* @throws UnsupportedOperationException always thrown
*/
@Override
public PrivateKey importPrivate(DhSpec spec) {
throw new UnsupportedOperationException();
}
}

View File

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

View File

@@ -64,7 +64,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <h2>Example</h2> <pre>{@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);

View File

@@ -80,10 +80,10 @@ import zeroecho.core.spec.ContextSpec;
*
* <h2>Example</h2> <pre>{@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());
* }</pre>
*

View File

@@ -51,8 +51,8 @@
* ad-hoc parameter generation.</li>
* <li>Expose predefined RFC 7919 FFDHE groups for safe parameter selection.
* </li>
* <li>Allow import/export of encoded keys via immutable key specs supporting
* PKCS#8 and X.509.</li>
* <li>Allow import/export of encoded keys via defensively copying key specs
* supporting PKCS#8 and X.509; private-key specs are destroyable.</li>
* </ul>
*
* <h2>Components</h2>
@@ -65,9 +65,10 @@
* {@link javax.crypto.spec.DHParameterSpec} instances.</li>
* <li><b>DhSpec</b>: immutable container for DH parameters; provides static
* factories for FFDHE groups (20488192 bits).</li>
* <li><b>DhPublicKeySpec</b> and <b>DhPrivateKeySpec</b>: immutable encoded key
* specs for importing/exporting X.509 and PKCS#8 encodings, with
* {@link zeroecho.core.marshal.PairSeq} marshalling support.</li>
* <li><b>DhPublicKeySpec</b> and <b>DhPrivateKeySpec</b>: 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.</li>
* </ul>
*
* <h2>Design notes</h2>

View File

@@ -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);

View File

@@ -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;
/**
* <h2>SHA-2, SHA-3, and SHAKE digest algorithms</h2>
@@ -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
);

View File

@@ -90,18 +90,18 @@ import zeroecho.core.context.MessageAgreementContext;
*
* <h2>Example</h2> <pre>{@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());
}
}

View File

@@ -63,7 +63,7 @@ import zeroecho.core.spec.ContextSpec;
*
* <h2>Usage</h2> <pre>{@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"

View File

@@ -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;
/**
* <h2>ECDH Key Pair Generator</h2>
*
* Implementation of {@link AsymmetricKeyBuilder} for elliptic curve
* Implementation of {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} for elliptic curve
* Diffie-Hellman (ECDH) key pairs.
*
* <p>
@@ -70,7 +68,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
*
* @since 1.0
*/
public final class EcdhKeyGenBuilder implements AsymmetricKeyBuilder<EcdhCurveSpec> {
public final class EcdhKeyGenBuilder implements AsymmetricKeyPairGenerator<EcdhCurveSpec> {
/**
* Generates a new elliptic curve key pair for use in ECDH key agreement.
*
@@ -92,40 +90,4 @@ public final class EcdhKeyGenBuilder implements AsymmetricKeyBuilder<EcdhCurveSp
kpg.initialize(new ECGenParameterSpec(spec.curveName()));
return kpg.generateKeyPair();
}
/**
* Unsupported operation for this builder.
*
* <p>
* Importing existing ECDH public keys should be performed via
* {@link EcdsaPublicKeyBuilder} with an {@link EcdsaPublicKeySpec}. This method
* will always throw an {@link UnsupportedOperationException}.
* </p>
*
* @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.
*
* <p>
* Importing existing ECDH private keys should be performed via
* {@link EcdsaPrivateKeyBuilder} with an {@link EcdsaPrivateKeySpec}. This
* method will always throw an {@link UnsupportedOperationException}.
* </p>
*
* @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.");
}
}

View File

@@ -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;
*
* <pre>{@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);
* }</pre>
*
* @since 1.0
@@ -104,8 +103,8 @@ public final class EcdsaAlgorithm extends AbstractCryptoAlgorithm {
* <p>
* 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.
* </p>
*/
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());
}
}

View File

@@ -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;
/**
* <h2>ECDSA Key Pair Generator</h2>
*
* 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}.
*
* <h2>Supported operations</h2>
* <ul>
* <li>{@link #generateKeyPair(EcdsaCurveSpec)} - create a fresh key pair for
* the given named curve.</li>
* <li>{@link #importPublic(EcdsaCurveSpec)} - unsupported; use
* {@link EcdsaPublicKeySpec} with {@link EcdsaPublicKeyBuilder} instead.</li>
* <li>{@link #importPrivate(EcdsaCurveSpec)} - unsupported; use
* {@link EcdsaPrivateKeySpec} with {@link EcdsaPrivateKeyBuilder} instead.</li>
* </ul>
* <p>The exact supported operation is
* {@link #generateKeyPair(EcdsaCurveSpec)}. Public and private import are
* registered separately through {@link EcdsaPublicKeyBuilder} and
* {@link EcdsaPrivateKeyBuilder}.</p>
*
* <h2>Usage</h2> Typically accessed indirectly through
* {@link CryptoAlgorithms#keyPair(String, zeroecho.core.spec.AlgorithmKeySpec)}
* or
* {@link CryptoAlgorithm#generateKeyPair(zeroecho.core.spec.AlgorithmKeySpec)}.
* <h2>Usage</h2> Typically accessed through the session key-operation API or
* {@link CryptoAlgorithm#asymmetricKeyPairGenerator(Class)}.
*
* <pre>{@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<EcdsaCurveSpec> {
public final class EcdsaKeyGenBuilder implements AsymmetricKeyPairGenerator<EcdsaCurveSpec> {
/**
* Generates a new elliptic curve key pair for the given curve specification.
*
@@ -93,38 +86,4 @@ public final class EcdsaKeyGenBuilder implements AsymmetricKeyBuilder<EcdsaCurve
kpg.initialize(new ECGenParameterSpec(spec.curveName()));
return kpg.generateKeyPair();
}
/**
* Unsupported operation for this builder.
*
* <p>
* Public key import should be performed using {@link EcdsaPublicKeySpec} and
* {@link EcdsaPublicKeyBuilder}.
* </p>
*
* @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.
*
* <p>
* Private key import should be performed using {@link EcdsaPrivateKeySpec} and
* {@link EcdsaPrivateKeyBuilder}.
* </p>
*
* @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.");
}
}

View File

@@ -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;
/**
* <h2>ECDSA Private Key Builder</h2>
*
* 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.
*
* <h2>Supported operations</h2>
* <ul>
* <li>{@link #importPrivate(EcdsaPrivateKeySpec)} - construct a
* {@link PrivateKey} instance from a PKCS#8 encoded key.</li>
* <li>{@link #generateKeyPair(EcdsaPrivateKeySpec)} - unsupported; use
* {@link EcdsaKeyGenBuilder} instead.</li>
* <li>{@link #importPublic(EcdsaPrivateKeySpec)} - unsupported; use
* {@link EcdsaPublicKeySpec} with {@link EcdsaPublicKeyBuilder} instead.</li>
* </ul>
* <p>The exact supported operation is
* {@link #importPrivate(EcdsaPrivateKeySpec)}. Generation and public import are
* registered through their own operation-specific implementations.</p>
*
* <h2>Encoding</h2> 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}.
*
* <h2>Usage</h2> Typically accessed indirectly through
* {@link CryptoAlgorithms#privateKey(String, zeroecho.core.spec.AlgorithmKeySpec)}
* or
* {@link CryptoAlgorithm#importPrivate(zeroecho.core.spec.AlgorithmKeySpec)}.
* <h2>Usage</h2> Typically accessed through the session key-operation API or
* {@link CryptoAlgorithm#privateKeyImporter(Class)}.
*
* <pre>{@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<EcdsaPrivateKeySpec> {
/**
* Unsupported operation for this builder.
*
* <p>
* ECDSA key pair generation should be performed using
* {@link EcdsaKeyGenBuilder}, not from a private key specification.
* </p>
*
* @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.
*
* <p>
* Public key import should be performed using {@link EcdsaPublicKeySpec} with
* {@link EcdsaPublicKeyBuilder}.
* </p>
*
* @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<EcdsaPrivateKeySpec> {
/**
* Imports a private key from a PKCS#8 encoded specification.
@@ -128,6 +87,11 @@ public final class EcdsaPrivateKeyBuilder implements AsymmetricKeyBuilder<EcdsaP
@Override
public PrivateKey importPrivate(EcdsaPrivateKeySpec spec) throws GeneralSecurityException {
KeyFactory kf = KeyFactory.getInstance("EC");
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded()));
byte[] encoded = spec.encoded();
try {
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
} finally {
Arrays.fill(encoded, (byte) 0);
}
}
}

View File

@@ -33,7 +33,11 @@
******************************************************************************/
package zeroecho.core.alg.ecdsa;
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;
/**
* <h2>ECDSA Private Key Specification</h2>
*
* 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");
}
}
}

View File

@@ -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;
/**
* <h2>ECDSA Public Key Builder</h2>
*
* 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.
*
* <h2>Supported operations</h2>
* <ul>
* <li>{@link #importPublic(EcdsaPublicKeySpec)} - construct a {@link PublicKey}
* instance from an X.509-encoded key.</li>
* <li>{@link #generateKeyPair(EcdsaPublicKeySpec)} - unsupported; use
* {@link EcdsaKeyGenBuilder} instead.</li>
* <li>{@link #importPrivate(EcdsaPublicKeySpec)} - unsupported; use
* {@link EcdsaPrivateKeySpec} with {@link EcdsaPrivateKeyBuilder} instead.</li>
* </ul>
* <p>The exact supported operation is
* {@link #importPublic(EcdsaPublicKeySpec)}. Generation and private import are
* registered through their own operation-specific implementations.</p>
*
* <h2>Encoding</h2> 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}.
*
* <h2>Usage</h2> Typically accessed indirectly through
* {@link CryptoAlgorithms#publicKey(String, zeroecho.core.spec.AlgorithmKeySpec)}
* or {@link CryptoAlgorithm#importPublic(zeroecho.core.spec.AlgorithmKeySpec)}.
* <h2>Usage</h2> Typically accessed through the session key-operation API or
* {@link CryptoAlgorithm#publicKeyImporter(Class)}.
*
* <pre>{@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<EcdsaPublicKeySpec> {
/**
* Unsupported operation for this builder.
*
* <p>
* ECDSA key pair generation should be performed using
* {@link EcdsaKeyGenBuilder}, not from a public key specification.
* </p>
*
* @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<EcdsaPublicKeySpec> {
/**
* Imports a public key from an X.509 SubjectPublicKeyInfo specification.
@@ -112,21 +88,4 @@ public final class EcdsaPublicKeyBuilder implements AsymmetricKeyBuilder<EcdsaPu
KeyFactory kf = KeyFactory.getInstance("EC");
return kf.generatePublic(new X509EncodedKeySpec(spec.encoded()));
}
/**
* Unsupported operation for this builder.
*
* <p>
* Private key import should be performed using {@link EcdsaPrivateKeySpec} with
* {@link EcdsaPrivateKeyBuilder}.
* </p>
*
* @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.");
}
}

View File

@@ -36,9 +36,9 @@
*
* <p>
* 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.
* </p>
*
* <h2>Scope and responsibilities</h2>
@@ -66,8 +66,9 @@
* <li><b>EcdsaPublicKeyBuilder</b> and <b>EcdsaPrivateKeyBuilder:</b> import
* keys from X.509 and PKCS#8 encodings via
* {@link java.security.KeyFactory}.</li>
* <li><b>EcdsaPublicKeySpec</b> and <b>EcdsaPrivateKeySpec:</b> immutable
* wrappers around encoded keys with marshalling support.</li>
* <li><b>EcdsaPublicKeySpec</b> and <b>EcdsaPrivateKeySpec:</b> wrappers around
* encoded keys with marshalling support; the private-key form is
* destroyable.</li>
* </ul>
*
* <h2>Design notes</h2>

View File

@@ -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());
}
}

View File

@@ -38,7 +38,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEdDSAKeyGenBuilder;
/**
* <h2>Key-pair builder for Ed25519</h2>
*
* Concrete {@link zeroecho.core.spi.AsymmetricKeyBuilder} implementation for
* Concrete {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} implementation for
* generating Ed25519 key pairs.
*
* <p>
@@ -50,7 +50,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEdDSAKeyGenBuilder;
* <h2>Usage example</h2> <pre>{@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);
* }</pre>
*
* <h2>Thread-safety</h2> Instances of this builder are stateless and may be

View File

@@ -50,7 +50,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* <h2>Usage example</h2> <pre>{@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());
* }</pre>
*
* <h2>Thread-safety</h2> The default spec instance is immutable and safe to

View File

@@ -38,7 +38,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder;
/**
* <h2>Private key builder for Ed25519</h2>
*
* Concrete {@link zeroecho.core.spi.AsymmetricKeyBuilder} for importing and
* Concrete {@link zeroecho.core.spi.PrivateKeyImporter} for importing
* wrapping Ed25519 private keys.
*
* <p>
@@ -59,7 +59,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder;
* <h2>Usage example</h2> <pre>{@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);
* }</pre>
*
* <h2>Thread-safety</h2> Instances of this builder are stateless and may be

View File

@@ -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);
* }</pre>
*
* <h2>Thread-safety</h2> Instances are immutable. The internal key bytes are
* defensively copied on construction and retrieval, making this class safe to
* share across threads.
* <h2>Thread-safety</h2> 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");
}
}
}

View File

@@ -38,7 +38,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder;
/**
* <h2>Public key builder for Ed25519</h2>
*
* Concrete {@link zeroecho.core.spi.AsymmetricKeyBuilder} for importing and
* Concrete {@link zeroecho.core.spi.PublicKeyImporter} for importing
* wrapping Ed25519 public keys.
*
* <p>
@@ -59,7 +59,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder;
* <h2>Usage example</h2> <pre>{@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);
* }</pre>
*
* <h2>Thread-safety</h2> Instances of this builder are stateless and may be

View File

@@ -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);

View File

@@ -49,8 +49,8 @@
* enforces the 64-byte tag size.</li>
* <li>Expose builders for key-pair generation and for importing encoded
* public/private keys.</li>
* <li>Define immutable key specifications suitable for safe cloning and simple
* marshalling.</li>
* <li>Define defensively copying key specifications suitable for safe cloning
* and simple marshalling; private-key specifications are destroyable.</li>
* </ul>
*
* <h2>Components</h2>
@@ -63,9 +63,9 @@
* marker spec for producing key pairs.</li>
* <li><b>Ed25519PublicKeyBuilder</b> / <b>Ed25519PrivateKeyBuilder</b>:
* importers backed by JCA key factories.</li>
* <li><b>Ed25519PublicKeySpec</b> / <b>Ed25519PrivateKeySpec</b>: immutable
* wrappers over X.509 and PKCS#8 encodings, with defensive copying and simple
* base64 marshalling helpers.</li>
* <li><b>Ed25519PublicKeySpec</b> / <b>Ed25519PrivateKeySpec</b>: wrappers over
* X.509 and PKCS#8 encodings, with defensive copying and simple base64
* marshalling helpers; the private-key form is destroyable.</li>
* </ul>
*
* <h2>Design notes</h2>

View File

@@ -82,12 +82,13 @@ import zeroecho.core.spec.VoidSpec;
* <pre>{@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);
* }</pre>
*
@@ -128,7 +129,7 @@ public final class Ed448Algorithm extends AbstractCryptoAlgorithm {
* <pre>{@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);
* }</pre>
*/
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());
}
}

View File

@@ -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;
/**
* <h2>Ed448 Private Key Builder</h2>
@@ -66,8 +65,8 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* }</pre>
*
* <h2>Thread-safety</h2> 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

View File

@@ -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;
/**
* <h2>Ed448 Private Key Specification</h2>
*
* Immutable specification for an Ed448 private key in PKCS#8 encoding.
* Destroyable specification for an Ed448 private key in PKCS#8 encoding.
*
* <p>
* 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);
* }</pre>
*
* <h2>Thread-safety</h2> Instances are immutable and safe to share across
* threads.
* <h2>Thread-safety</h2> 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");
}
}
}

View File

@@ -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;
/**
* <h2>Ed448 Public Key Builder</h2>
@@ -65,8 +64,8 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* }</pre>
*
* <h2>Thread-safety</h2> 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

View File

@@ -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.
* </p>
*
* <h2>Scope and responsibilities</h2>
@@ -49,8 +50,8 @@
* enforces the 114-byte tag size.</li>
* <li>Expose builders for key-pair generation and for importing encoded
* keys.</li>
* <li>Define immutable key specifications suitable for safe cloning and simple
* marshalling.</li>
* <li>Define defensively copying key specifications suitable for safe cloning
* and simple marshalling; private-key specifications are destroyable.</li>
* </ul>
*
* <h2>Components</h2>
@@ -63,9 +64,9 @@
* marker spec for producing key pairs.</li>
* <li><b>Ed448PublicKeyBuilder</b> / <b>Ed448PrivateKeyBuilder</b>: importers
* backed by JCA key factories.</li>
* <li><b>Ed448PublicKeySpec</b> / <b>Ed448PrivateKeySpec</b>: immutable
* wrappers over X.509 and PKCS#8 encodings, with defensive copying and base64
* marshalling helpers.</li>
* <li><b>Ed448PublicKeySpec</b> / <b>Ed448PrivateKeySpec</b>: wrappers over
* X.509 and PKCS#8 encodings, with defensive copying and base64 marshalling
* helpers; the private-key form is destroyable.</li>
* </ul>
*
* <h2>Design notes</h2>

View File

@@ -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;
/**
* <h2>ElGamal Asymmetric Encryption Algorithm</h2>
@@ -110,11 +113,12 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
*
* <h2>Example</h2> <pre>{@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());
* }</pre>
*
@@ -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);
});
}
/**

View File

@@ -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();
}
/**

View File

@@ -71,7 +71,7 @@ import zeroecho.core.spec.ContextSpec;
*
* <h2>Usage</h2> Instances are created via the static factories: <pre>{@code
* ElgamalEncSpec spec = ElgamalEncSpec.pkcs1();
* EncryptionContext ctx = algo.create(KeyUsage.ENCRYPT, pubKey, spec);
* EncryptionContext ctx = algo.createContext(KeyUsage.ENCRYPT, pubKey, spec);
* }</pre>
*
* <h2>Thread-safety</h2> {@code ElgamalEncSpec} is immutable and safe to share

View File

@@ -64,7 +64,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* <h2>Usage</h2> <pre>{@code
* ElgamalKeyGenSpec spec = ElgamalKeyGenSpec.elgamal2048();
* KeyPair kp = algo.generateKeyPair(spec);
* KeyPair kp = algo.asymmetricKeyPairGenerator(ElgamalKeyGenSpec.class).generateKeyPair(spec);
* }</pre>
*
* <h2>Thread-safety</h2> Instances are immutable and can be freely shared

View File

@@ -73,7 +73,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* <h2>Usage</h2> <pre>{@code
* ElgamalParamSpec spec = ElgamalParamSpec.ffdhe2048();
* KeyPair kp = algo.generateKeyPair(spec);
* KeyPair kp = algo.asymmetricKeyPairGenerator(ElgamalParamSpec.class).generateKeyPair(spec);
* }</pre>
*
* <h2>Thread-safety</h2> Instances are immutable and safe to share between

View File

@@ -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.</li>
* </ul>
*
* <h2>Thread-safety</h2> Instances are immutable and safe to share between
* threads.
* <h2>Thread-safety</h2> 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");
}
}
}

View File

@@ -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);

View File

@@ -70,9 +70,9 @@
* <li><b>ElgamalKeyGenSpec</b>: parameters for generating fresh domain
* parameters and key pairs; typically disabled in favor of predefined parameter
* sets.</li>
* <li><b>ElgamalPublicKeySpec</b> / <b>ElgamalPrivateKeySpec</b>: immutable
* encoded key specifications (X.509 and PKCS#8) with defensive copying and
* compact marshalling helpers.</li>
* <li><b>ElgamalPublicKeySpec</b> / <b>ElgamalPrivateKeySpec</b>: encoded key
* specifications (X.509 and PKCS#8) with defensive copying and compact
* marshalling helpers; the private-key form is destroyable.</li>
* </ul>
*
* <h2>Design notes</h2>

View File

@@ -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;
/**
* <h2>Frodo Key Encapsulation Mechanism (KEM)</h2>
@@ -104,9 +107,8 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* X.509 encoding.</li>
* <li>Private keys may be imported from {@link FrodoPrivateKeySpec} using a
* PKCS#8 encoding.</li>
* <li>Direct import of key specs via {@code generateKeyPair} in the spec-based
* builders is not supported and will throw
* {@link UnsupportedOperationException}.</li>
* <li>Generation and public/private import are registered as independent exact
* capabilities.</li>
* </ul>
*
* <h2>Provider requirements</h2>
@@ -120,13 +122,14 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* <h2>Example usage</h2> <pre>{@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);
* }</pre>
*
* <h2>Thread-safety</h2>
@@ -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 {

View File

@@ -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;
* <h2>Specification for importing a Frodo private key</h2>
*
* {@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.
*
* <h2>Encoding format</h2>
@@ -59,9 +62,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <h2>Usage</h2>
* <ul>
* <li>Instances of this spec can be passed to
* {@link CryptoAlgorithm#importPrivate(AlgorithmKeySpec)
* importPrivate(FrodoPrivateKeySpec)} to construct a usable
* {@link java.security.PrivateKey} object.</li>
* {@link zeroecho.sdk.KeyBuilders.Asymmetric#importPrivate(String, AlgorithmKeySpec)}
* to construct a usable {@link java.security.PrivateKey} object.</li>
* <li>The {@link #marshal(FrodoPrivateKeySpec)} and {@link #unmarshal(PairSeq)}
* helpers allow safe conversion to/from structured textual form for persistence
* or transmission.</li>
@@ -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);
* }</pre>
*
* <h2>Thread-safety</h2>
* <p>
* 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.
* </p>
*
* @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");
}
}
}

View File

@@ -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;
* <h2>Usage</h2>
* <ul>
* <li>Instances of this spec can be passed to
* {@link CryptoAlgorithm#importPublic(AlgorithmKeySpec)
* importPublic(FrodoPublicKeySpec)} to construct a usable
* {@link java.security.PublicKey}.</li>
* {@link zeroecho.sdk.KeyBuilders.Asymmetric#importPublic(String, AlgorithmKeySpec)}
* to construct a usable {@link java.security.PublicKey}.</li>
* <li>The {@link #marshal(FrodoPublicKeySpec)} and {@link #unmarshal(PairSeq)}
* methods allow safe serialization into and recovery from structured textual
* form.</li>
@@ -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);
* }</pre>
*
* <h2>Thread-safety</h2>

View File

@@ -51,8 +51,9 @@
* role for initiator/responder workflows.</li>
* <li>Provide a {@link zeroecho.core.context.KemContext} implementation bound
* to either a public or private key for encapsulation or decapsulation.</li>
* <li>Expose immutable specifications for key generation variants and encoded
* key carriers with marshalling helpers.</li>
* <li>Expose immutable key-generation specifications and defensively copying
* encoded-key carriers with marshalling helpers; private-key carriers are
* destroyable.</li>
* <li>Ensure operations are delegated to a supported PQC provider (BouncyCastle
* PQC) and fail fast if absent.</li>
* </ul>

View File

@@ -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;
/**
* <h2>HMAC Algorithm Integration</h2>
@@ -86,11 +88,11 @@ import zeroecho.core.spi.SymmetricKeyBuilder;
*
* <h2>Usage example</h2> <pre>{@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);
});
}
}

View File

@@ -55,12 +55,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* <h2>Usage</h2> 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}:
*
* <pre>{@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);
* }</pre>
*
* <h2>Defaults</h2> Convenience static factories are provided for the most

View File

@@ -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);
* }
* </pre>
*
@@ -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);
}
}

View File

@@ -100,7 +100,7 @@ import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate;
* <h2>Usage example</h2>
* <h3>Produce HMAC-SHA256 trailer</h3> <pre>
* {@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;
}

View File

@@ -54,10 +54,10 @@ import zeroecho.core.spec.ContextSpec;
*
* <h2>Usage</h2> This spec is passed when creating a new HMAC context:
* <pre>{@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);

View File

@@ -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
* }
* }
* </pre>
*/
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<byte[]> verificationStrategy) {
super(upstream, 8192);
this.mac = mac;

View File

@@ -48,8 +48,8 @@
* <li>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.</li>
* <li>Provide immutable specs for selecting the HMAC variant and for supplying
* keys (generation or import of raw key material).</li>
* <li>Provide immutable specs for selecting the HMAC variant and destroyable
* specs for importing raw key material.</li>
* <li>Encapsulate JCA/JCE interop and provider checks behind small
* factories.</li>
* </ul>
@@ -68,7 +68,7 @@
* specific HMAC variant.</li>
* <li><b>HmacKeyImportSpec</b>: wrapper for importing existing raw keys, with
* Base64/hex helpers.</li>
* <li><b>Stream</b>: internal passthrough input stream implementing the
* <li><b>HmacStream</b>: internal passthrough input stream implementing the
* byte-pumping and trailer/verification logic for the MAC context.</li>
* </ul>
*

View File

@@ -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;
/**
* <h2>HQC (Hamming Quasi-Cyclic) Algorithm Integration</h2>
@@ -124,13 +127,14 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* <h2>Usage example</h2> <pre>{@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);
* }</pre>
*
* @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 {

View File

@@ -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);
* }</pre>
*
* @since 1.0

View File

@@ -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;
*
* <p>
* 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.
* </p>
*
* <h2>Serialization</h2>
@@ -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");
}
}
}

View File

@@ -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);

View File

@@ -49,8 +49,9 @@
* workflows.</li>
* <li>Provide a {@link zeroecho.core.context.KemContext} bound to a public or
* private key for encapsulation or decapsulation.</li>
* <li>Expose immutable specifications for key generation variants and encoded
* key carriers with simple marshalling helpers.</li>
* <li>Expose immutable key-generation specifications and defensively copying
* encoded-key carriers with simple marshalling helpers; private-key carriers
* are destroyable.</li>
* <li>Ensure operations use a supported PQC provider and fail fast if the
* provider is absent.</li>
* </ul>

View File

@@ -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);
* }</pre>
*/
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());
* }</pre>
*/
@@ -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);
});
}
/**

View File

@@ -54,7 +54,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <h2>Usage</h2> Instances of this class are passed to the Kyber key builder to
* select the desired parameter set: <pre>{@code
* CryptoAlgorithm kyber = new KyberAlgorithm();
* KeyPair kp = kyber.asymmetricKeyBuilder(KyberKeyGenSpec.class)
* KeyPair kp = kyber.asymmetricKeyPairGenerator(KyberKeyGenSpec.class)
* .generateKeyPair(KyberKeyGenSpec.kyber768());
* }</pre>
*
@@ -63,7 +63,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* </p>
*
* @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 {
/**

View File

@@ -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.
*
* <p>
* 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 providers native representation.
* </p>
@@ -51,7 +55,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <h2>Encoding</h2>
* <ul>
* <li>Format: PKCS#8 DER encoding of a Kyber private key.</li>
* <li>Stored as a defensive clone to ensure immutability.</li>
* <li>Stored as a defensive clone.</li>
* <li>Marshalling/unmarshalling supported via {@link PairSeq} with Base64
* encoding.</li>
* </ul>
@@ -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;
* }</pre>
*
* <p>
* This class is immutable and thread-safe.
* Access and destruction are synchronized.
* </p>
*
* @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");
}
}
}

View File

@@ -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);

View File

@@ -50,8 +50,9 @@
* message-style agreement adapter where needed.</li>
* <li>Provide a {@link zeroecho.core.context.KemContext} implementation bound
* to either a public or private key for encapsulation or decapsulation.</li>
* <li>Expose immutable specifications for key generation variants and encoded
* key carriers with compact marshalling helpers.</li>
* <li>Expose immutable key-generation specifications and defensively copying
* encoded-key carriers with compact marshalling helpers; private-key carriers
* are destroyable.</li>
* <li>Ensure operations are delegated to an available PQC provider and fail
* fast if the provider is absent.</li>
* </ul>

View File

@@ -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());
}
}

View File

@@ -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<MldsaKeyGenSpec> {
public final class MldsaKeyGenBuilder implements AsymmetricKeyPairGenerator<MldsaKeyGenSpec> {
private static final String ALG_PURE = "MLDSA";
private static final String ALG_SHA512 = "SHA512withMLDSA";
@@ -83,30 +83,6 @@ public final class MldsaKeyGenBuilder implements AsymmetricKeyBuilder<MldsaKeyGe
return kpg.generateKeyPair();
}
/**
* Key generation specs cannot import public keys.
*
* @param spec key generation specification
* @return never returns normally
* @throws UnsupportedOperationException always
*/
@Override
public java.security.PublicKey importPublic(MldsaKeyGenSpec spec) {
throw new UnsupportedOperationException("Use MldsaPublicKeySpec to import a public key.");
}
/**
* Key generation specs cannot import private keys.
*
* @param spec key generation specification
* @return never returns normally
* @throws UnsupportedOperationException always
*/
@Override
public java.security.PrivateKey importPrivate(MldsaKeyGenSpec spec) {
throw new UnsupportedOperationException("Use MldsaPrivateKeySpec to import a private key.");
}
private static Object resolveBcParameterSpec(MldsaKeyGenSpec spec) throws GeneralSecurityException {
if (spec.explicitParamConstant() != null) {
Object c = fetchStaticField("org.bouncycastle.jcajce.spec.MLDSAParameterSpec",

View File

@@ -35,46 +35,21 @@ 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.PKCS8EncodedKeySpec;
import java.util.Arrays;
import zeroecho.core.spi.AsymmetricKeyBuilder;
import zeroecho.core.spi.PrivateKeyImporter;
/**
* Builder for importing ML-DSA private keys from encoded specifications.
*
* @since 1.0
*/
public final class MldsaPrivateKeyBuilder implements AsymmetricKeyBuilder<MldsaPrivateKeySpec> {
public final class MldsaPrivateKeyBuilder implements PrivateKeyImporter<MldsaPrivateKeySpec> {
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<MldsaP
public PrivateKey importPrivate(MldsaPrivateKeySpec 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);
}
}
}

View File

@@ -33,7 +33,11 @@
******************************************************************************/
package zeroecho.core.alg.mldsa;
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 ML-DSA private key.
*
* <p>
* {@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.
* </p>
@@ -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);
}
}

View File

@@ -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<MldsaPublicKeySpec> {
public final class MldsaPublicKeyBuilder implements PublicKeyImporter<MldsaPublicKeySpec> {
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<MldsaPu
: KeyFactory.getInstance(ALG, spec.providerName());
return kf.generatePublic(new X509EncodedKeySpec(spec.encoded()));
}
/**
* Private key import is not supported by this spec.
*
* @param spec encoded public key spec
* @return never returns normally
* @throws UnsupportedOperationException always
*/
@Override
public PrivateKey importPrivate(MldsaPublicKeySpec spec) {
throw new UnsupportedOperationException("Use MldsaPrivateKeySpec for private keys.");
}
}

View File

@@ -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;
/**
* NtruAlgorithm exposes the NTRU KEM from the Bouncy Castle PQC provider and
@@ -90,15 +93,15 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* <pre>{@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());
* }</pre>
*
@@ -107,18 +110,18 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* </p>
* <pre>{@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);
});
}
/**

View File

@@ -49,7 +49,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <h2>Usage</h2> <pre>{@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());
* }</pre>
*

View File

@@ -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.
*
* <p>
* 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.
* </p>
*
* <h2>Usage</h2> <pre>{@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");
}
}
}

View File

@@ -48,8 +48,9 @@
* DECAPSULATE roles, with an optional message-style agreement adapter.</li>
* <li>Provide a {@link zeroecho.core.context.KemContext} bound to either a
* public key (encapsulation) or a private key (decapsulation).</li>
* <li>Expose immutable specifications for key generation variants and encoded
* key carriers with simple marshalling helpers.</li>
* <li>Expose immutable key-generation specifications and defensively copying
* encoded-key carriers with simple marshalling helpers; private-key carriers
* are destroyable.</li>
* <li>Validate the presence of a suitable PQC provider before performing JCA
* operations.</li>
* </ul>

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