From 49dc080c65918f1729223e17900936bd7e3b794b Mon Sep 17 00:00:00 2001
From: Leo Galambos
* In both cases, the created context is consumed by
- * {@link MultiRecipientDataSourceBuilder#addRecipient(Object)} and is closed
- * internally by the builder.
+ * the matching {@link MultiRecipientDataSourceBuilder} recipient method and is
+ * closed internally by the resulting content.
*
- * A {@code Capability} describes one role supported by a - * {@link CryptoAlgorithm}, including: - *
- *- * Each capability corresponds to a call to - * {@link AbstractCryptoAlgorithm#capability(AlgorithmFamily, KeyUsage, Class, Class, Class, ContextConstructorKS, Supplier)}. - *
- * - *The default specification is resolved once during provider construction. + * All components therefore have stable value semantics and are safe for + * concurrent reads.
* + * @param algorithmId canonical algorithm identifier + * @param family algorithm family + * @param role supported key usage + * @param contextType produced context type + * @param keyType accepted key type + * @param specType accepted specification type + * @param defaultSpec non-null resolved default specification * @since 1.0 */ public record Capability(String algorithmId, AlgorithmFamily family, KeyUsage role, - Class 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()); + } } } diff --git a/lib/src/main/java/zeroecho/core/CryptoAlgorithm.java b/lib/src/main/java/zeroecho/core/CryptoAlgorithm.java index 07cae54..39f2337 100644 --- a/lib/src/main/java/zeroecho/core/CryptoAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/CryptoAlgorithm.java @@ -33,32 +33,29 @@ ******************************************************************************/ package zeroecho.core; -import java.io.IOException; -import java.security.GeneralSecurityException; import java.security.Key; -import java.security.KeyPair; -import java.security.PrivateKey; -import java.security.PublicKey; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.EnumMap; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Supplier; -import javax.crypto.SecretKey; - import zeroecho.core.context.CryptoContext; import zeroecho.core.err.UnsupportedRoleException; import zeroecho.core.err.UnsupportedSpecException; import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.ContextSpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; -import zeroecho.core.spi.ContextConstructorKS; -import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.ContextFactoryKS; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; +import zeroecho.core.spi.SymmetricKeyGenerator; +import zeroecho.core.spi.SymmetricKeyImporter; /** * Abstract base class for all cryptographic algorithm definitions in ZeroEcho. @@ -70,8 +67,7 @@ import zeroecho.core.spi.SymmetricKeyBuilder; * signatures. ** Security note: Algorithms must enforce strong validation of keys and - * specs during registration and {@link #create(KeyUsage, Key, ContextSpec)} to + * specs during registration and + * {@link #createContext(KeyUsage, Key, ContextSpec)} to * prevent downgrade or misuse attacks. *
* @@ -123,6 +114,8 @@ import zeroecho.core.spi.SymmetricKeyBuilder; */ public abstract class CryptoAlgorithm { // NOPMD + private static final String SPEC_TYPE_NULL = "specType must not be null"; + private final String _id; private final String _displayName; private final int _priority; @@ -130,8 +123,18 @@ public abstract class CryptoAlgorithm { // NOPMD private final List* Concrete algorithms call this during construction to declare support for * specific roles (e.g., {@code ENCRYPT}, {@code VERIFY}). When - * {@link #create(KeyUsage, Key, ContextSpec)} is later invoked, the provided + * {@link #createContext(KeyUsage, Key, ContextSpec)} is later invoked, the provided * {@code key} and optional {@code spec} are matched against these bindings. *
* @@ -309,9 +312,15 @@ public abstract class CryptoAlgorithm { // NOPMD * @param- * Used for discovery and documentation (e.g., tool UIs). - *
- */ - 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- * Each {@code AsymEntry} is keyed by a specific {@link AlgorithmKeySpec} - * subtype. It holds the {@link AsymmetricKeyBuilder} instance capable of - * generating or importing keys for that spec, and an optional supplier that - * provides a safe default spec (if the algorithm wants to support "generate - * with defaults"). - *
+ *The optional default is resolved and validated during registration. + * Registered generators must be safe for concurrent invocation after the + * algorithm is published.
* - *- * Concrete algorithms call this during construction. The {@code specType} acts - * as a key for later lookup and must be unique within this algorithm. - *
- * - * @param specType the spec class accepted by {@code builder} - * @param builder builder that can generate/import keys for - * {@code specType} - * @param defaultKeySpecOrNull optional supplier for a default spec (may be - * {@code null}) - * @param- * The default spec value is best-effort; suppliers may throw, in which case - * {@code defaultKeySpec} is reported as {@code null}. - *
- * - * @return immutable list of {@link AsymBuilderInfo} descriptors + * @param specType exact specification class + * @param importer non-null importer safe for concurrent invocation + * @param- * Each {@code SymBuilderInfo} describes the specification type that a - * {@link SymmetricKeyBuilder} can handle, along with an optional default - * specification object. These descriptors are used for discovery and - * documentation purposes, for example when rendering catalog information in - * tooling or UIs. - *
+ *The optional default is resolved and validated during registration.
* - *- * 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. - *
- * - *The returned implementation may be shared and invoked concurrently.
+ * + * @param specType exact specification class; subclasses are not matched + * @param- * The default spec value is best-effort; suppliers may throw, in which case - * {@code defaultKeySpec} is reported as {@code null}. - *
+ *The returned implementation may be shared and invoked concurrently.
* - * @return immutable list of {@link SymBuilderInfo} descriptors - */ - public final ListThe returned implementation may be shared and invoked concurrently.
+ * + * @param specType exact specification class; subclasses are not matched + * @paramThe returned implementation may be shared and invoked concurrently.
* - *- * This convenience method iterates over all registered asymmetric key builders - * that declare a non-null default {@link AlgorithmKeySpec} supplier. For each, - * it attempts to obtain the default spec and generate a key pair. If a builder - * fails (e.g., the builder only supports import or rejects the parameters), the - * method records the failure and continues with the next candidate. - *
- * - *{@code
- * CryptoAlgorithm algo = CryptoAlgorithms.require("Ed25519");
- * KeyPair kp = algo.generateKeyPair();
- * }
- *
- * @return a newly generated key pair using a default spec from one of the
- * registered asymmetric builders
- * @throws IllegalStateException if no builder declares a default spec
- * supplier
- * @throws GeneralSecurityException if all candidate builders fail to generate a
- * key pair; the exception message details
- * individual causes
- */
- public final KeyPair generateKeyPair() throws GeneralSecurityException {
- StringBuilder reasons = new StringBuilder(128);
- boolean attempted = false;
-
- for (Map.EntryThe returned implementation may be shared and invoked concurrently.
+ * + * @param specType exact specification class; subclasses are not matched + * @param- * {@code CryptoAlgorithms} discovers algorithms via {@link ServiceLoader} and - * exposes: - *
- *Providers are discovered once through {@link ServiceLoader}, sorted by + * canonical algorithm identifier, and retained in one immutable registry. + * Runtime policy and auditing belong exclusively to explicitly created + * {@link zeroecho.sdk.ZeroEchoSession} instances.
* * @since 1.0 */ public final class CryptoAlgorithms { - - private static final Map- * The returned set is backed by an unmodifiable registry snapshot. Use these - * identifiers with {@link #require(String)} or the convenience methods below. - *
- * - * @return unmodifiable set of canonical algorithm ids + * @return unmodifiable set of canonical identifiers */ public static Set- * If the id is unknown, an {@link IllegalArgumentException} is thrown. This - * method is preferred over direct access to ensure consistent error handling - * and to centralize future selection logic. - *
- * - * @param id canonical algorithm identifier (e.g., {@code "AES/GCM"} or - * {@code "Ed25519"}) - * @return the corresponding {@link CryptoAlgorithm} implementation - * @throws IllegalArgumentException if no algorithm is registered under + * @param id canonical algorithm identifier + * @return registered algorithm + * @throws IllegalArgumentException if no algorithm is registered with * {@code id} */ public static CryptoAlgorithm require(String id) { - CryptoAlgorithm a = BY_ID.get(id); - if (a == null) { + CryptoAlgorithm algorithm = BY_ID.get(id); + if (algorithm == null) { throw new IllegalArgumentException("Unknown algorithm id: " + id); } - return a; - } - - /** - * Sets the global cryptographic policy applied before any context creation. - * - *- * Pass {@code null} to revert to a permissive policy. Policies should be fast - * and side-effect free; they are invoked on every - * {@link #create(String, KeyUsage, Key, ContextSpec)} call. - *
- * - * @param p policy to install, or {@code null} to use - * {@link CryptoPolicy#permissive()} - */ - public static void setPolicy(CryptoPolicy- * Pass {@code null} to disable custom auditing (a no-op listener will be - * installed). The listener may be invoked by context proxies (in - * {@link AuditMode#WRAP}) and by the convenience key factory methods below. - *
- * - * @param l listener instance or {@code null} for a no-op listener - */ - public static void setAuditListener(AuditListener l) { - AUDIT = (l == null ? AuditListener.noop() : l); - } - - /** - * Returns the current global {@link AuditListener}. - * - * @return the active audit listener (never {@code null}) - */ - public static AuditListener audit() { - return AUDIT; - } - - /** - * Declares how auditing is applied to cryptographic contexts. - * - *- * The {@code AuditMode} controls whether contexts created by - * {@link CryptoAlgorithms#create(String, KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)} - * are wrapped in auditing proxies or whether auditing is delegated entirely to - * the caller. - *
- * - *- * Only explicit events emitted here (e.g., - * {@link AuditListener#onContextCreated}) are sent to the listener; - * stream-level or per-operation auditing is not injected. - *
- */ - OFF, - /** - * Wraps supported contexts in dynamic proxies that emit stream-level auditing. - * - *- * In this mode, creation events are emitted by the proxy rather than here, and - * subsequent operations (e.g., updates, finalization) may also be audited - * depending on the proxy implementation. - *
- */ - WRAP, - /** - * No wrapping and no automatic events. - * - *- * The caller is responsible for emitting all relevant audit events via the - * {@link #audit()} listener. - *
- */ - MANUAL - } - - /** - * Sets the auditing mode for subsequently created contexts. - * - *- * Passing {@code null} resets the mode to {@link AuditMode#OFF}. - *
- * - * @param mode desired auditing strategy or {@code null} for {@code OFF} - */ - public static void setAuditMode(AuditMode mode) { - AUDIT_MODE = (mode == null ? AuditMode.OFF : mode); - } - - /** - * Returns the current auditing mode. - * - * @return active {@link AuditMode}; never {@code null} - */ - public static AuditMode getAuditMode() { - return AUDIT_MODE; - } - - /** - * Creates a {@link CryptoContext} for the given algorithm id and role, applying - * policy validation and optional auditing/wrapping. - * - *- * Flow: - *
- *- * The returned context remains owned by the caller of the factory method. This - * helper does not acquire an additional resource requiring local cleanup. - *
- * - * @param- * Equivalent to {@code create(id, role, key, null)}. - *
- * - * @param id canonical algorithm identifier - * @param role desired {@link KeyUsage} - * @param key key instance for the role - * @param- * Emits - * {@link AuditListener#onKeyGenerated(String, String, AlgorithmKeySpec, KeyPair)} - * on success. - *
- * - * @param id canonical algorithm identifier - * @param spec algorithm-specific key specification - * @param- * Emits {@link AuditListener#onKeyBuilt(String, String, AlgorithmKeySpec, Key)} - * on success. - *
- * - * @param id canonical algorithm identifier - * @param spec algorithm-specific key specification containing encoded public - * material - * @param- * Emits {@link AuditListener#onKeyBuilt(String, String, AlgorithmKeySpec, Key)} - * on success. - *
- * - * @param id canonical algorithm identifier - * @param spec algorithm-specific key specification containing encoded private - * material - * @param- * Emits {@link AuditListener#onKeyBuilt(String, String, AlgorithmKeySpec, Key)} - * on success. - *
- * - * @param id canonical algorithm identifier - * @param spec algorithm-specific key specification containing raw/encoded - * material - * @param- * If destruction succeeds, - * {@link AuditListener#onKeyDestroyed(String, String, Key)} is emitted. Any - * exceptions from {@code destroy()} are swallowed; the method returns - * {@code false} when destruction did not occur. - *
- * - * @param algoId algorithm identifier used for audit metadata - * @param provider provider name used for audit metadata - * @param key key to destroy - * @return {@code true} if the key reported destroyed, {@code false} otherwise - */ - public static boolean destroyKey(String algoId, String provider, Key key) { - boolean destroyed = false; - try { - if (key instanceof javax.security.auth.Destroyable) { - javax.security.auth.Destroyable d = (javax.security.auth.Destroyable) key; - if (!d.isDestroyed()) { - d.destroy(); - destroyed = true; - } - } - } catch (Exception ignored) { - // swallow and report via audit only if destroyed - } - if (destroyed) { - AUDIT.onKeyDestroyed(algoId, provider, key); - } - return destroyed; - } - - /** - * Convenience wrapper for - * {@link CryptoAlgorithm#generateSecret(AlgorithmKeySpec)}. - * - * @param id canonical algorithm identifier - * @param spec algorithm-specific key specification - * @param* {@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: *
* *- * Note: Default spec / key-spec values shown in outputs are derived from - * {@code Supplier}s registered by algorithms. Suppliers may compute labels or - * return lightweight descriptors; their intent is documentation, not - * round‑tripping. + * Note: Default spec / key-spec values shown in outputs are stable + * metadata values resolved when providers are initialized; their intent is + * documentation, not round-tripping. *
* * @since 1.0 @@ -93,26 +89,25 @@ public final class CryptoCatalog { } /** - * Discovers {@link CryptoAlgorithm} implementations via {@link ServiceLoader} - * and returns an immutable catalog snapshot. + * Returns a catalog view of the authoritative registry initialized by + * {@link CryptoAlgorithms}. * *- * During loading, algorithm ids are checked for uniqueness. A duplicate id - * results in an {@link IllegalStateException} to prevent ambiguous resolution. + * Provider discovery, deterministic ordering, and duplicate checking occur + * once in {@code CryptoAlgorithms}. This method neither scans providers nor + * copies their collection. *
* * @return an immutable {@code CryptoCatalog} with all discovered algorithms - * @throws IllegalStateException if two providers declare the same algorithm id + * @throws ExceptionInInitializerError if authoritative provider initialization + * fails */ public static CryptoCatalog load() { - Map* This class wraps raw key material (16, 24, or 32 bytes) for use with the AES * algorithm. Factory methods support construction from raw bytes, hex strings, - * or Base64-encoded strings. The key material is defensively copied to maintain - * immutability. + * or Base64-encoded strings. The key material is defensively copied. *
* *@@ -58,13 +60,16 @@ import zeroecho.core.spec.AlgorithmKeySpec; *
* *- * Objects of this type are immutable and thread-safe. + * Objects of this type are thread-safe while active and may be destroyed to wipe + * their owned key bytes. Access and marshalling fail after destruction. *
* * @since 1.0 */ -public final class AesKeyImportSpec implements AlgorithmKeySpec { +public final class AesKeyImportSpec implements AlgorithmKeySpec, Destroyable { private final byte[] key; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; private AesKeyImportSpec(byte[] key) { Objects.requireNonNull(key, "key must not be null"); @@ -96,7 +101,12 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec { */ public static AesKeyImportSpec fromHex(String hex) { Objects.requireNonNull(hex, "hex must not be null"); - return fromRaw(HexFormat.of().parseHex(hex)); + byte[] decoded = HexFormat.of().parseHex(hex); + try { + return fromRaw(decoded); + } finally { + Arrays.fill(decoded, (byte) 0); + } } /** @@ -109,7 +119,12 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec { */ public static AesKeyImportSpec fromBase64(String b64) { Objects.requireNonNull(b64, "base64 must not be null"); - return fromRaw(Base64.getDecoder().decode(b64)); + byte[] decoded = Base64.getDecoder().decode(b64); + try { + return fromRaw(decoded); + } finally { + Arrays.fill(decoded, (byte) 0); + } } /** @@ -118,7 +133,13 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec { * @return the raw key material */ public byte[] key() { - return Arrays.copyOf(key, key.length); + lifecycleLock.lock(); + try { + ensureActive(); + return Arrays.copyOf(key, key.length); + } finally { + lifecycleLock.unlock(); + } } /** @@ -129,7 +150,7 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec { * @return a sequence containing the key data */ public static PairSeq marshal(AesKeyImportSpec spec) { - String k = Base64.getEncoder().withoutPadding().encodeToString(spec.key); + String k = spec.encodedKey(); return PairSeq.of("type", "AES-KEY", "k.b64", k); } @@ -143,21 +164,81 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec { */ public static AesKeyImportSpec unmarshal(PairSeq p) { byte[] out = null; - PairSeq.Cursor cur = p.cursor(); - while (cur.next()) { - String k = cur.key(); - String v = cur.value(); - switch (k) { - case "k.b64" -> out = Base64.getDecoder().decode(v); - case "k.hex" -> out = HexFormat.of().parseHex(v); - case "k.raw" -> out = v.getBytes(StandardCharsets.ISO_8859_1); - default -> { - /* ignore */ } + try { + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + switch (k) { + case "k.b64" -> { + wipe(out); + out = Base64.getDecoder().decode(v); + } + case "k.hex" -> { + wipe(out); + out = HexFormat.of().parseHex(v); + } + case "k.raw" -> { + wipe(out); + out = v.getBytes(StandardCharsets.ISO_8859_1); + } + default -> { + /* ignore */ } + } } + if (out == null) { + throw new IllegalArgumentException("AES key missing (k.b64 / k.hex / k.raw)"); + } + return new AesKeyImportSpec(out); + } finally { + wipe(out); } - if (out == null) { - throw new IllegalArgumentException("AES key missing (k.b64 / k.hex / k.raw)"); + } + + private static void wipe(byte[] current) { + if (current != null) { + Arrays.fill(current, (byte) 0); + } + } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(key); + } finally { + lifecycleLock.unlock(); + } + } + + /** {@inheritDoc} */ + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(key, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + /** {@inheritDoc} */ + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("AES key import specification has been destroyed"); } - return new AesKeyImportSpec(out); } } diff --git a/lib/src/main/java/zeroecho/core/alg/bike/BikeAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/bike/BikeAlgorithm.java index efadda0..5ef65fc 100644 --- a/lib/src/main/java/zeroecho/core/alg/bike/BikeAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/bike/BikeAlgorithm.java @@ -44,6 +44,7 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; import java.security.spec.X509EncodedKeySpec; import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; @@ -56,7 +57,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; import zeroecho.core.context.KemContext; import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.spec.VoidSpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; /** *{@code
* // Generate a BIKE-192 key pair
- * KeyPair kp = bikeAlgorithm.asymmetricKeyBuilder(BikeKeyGenSpec.class)
+ * KeyPair kp = bikeAlgorithm.asymmetricKeyPairGenerator(BikeKeyGenSpec.class)
* .generateKeyPair(BikeKeyGenSpec.bike192());
* }
*
diff --git a/lib/src/main/java/zeroecho/core/alg/bike/BikePrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/bike/BikePrivateKeySpec.java
index d4e349d..e73a13e 100644
--- a/lib/src/main/java/zeroecho/core/alg/bike/BikePrivateKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/bike/BikePrivateKeySpec.java
@@ -33,8 +33,12 @@
******************************************************************************/
package zeroecho.core.alg.bike;
+import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq;
import zeroecho.core.marshal.PairSeq.Cursor;
@@ -49,7 +53,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* {@code
* // Import a BIKE private key
* BikePrivateKeySpec spec = new BikePrivateKeySpec(pkcs8Bytes);
- * PrivateKey key = bikeAlgorithm.importPrivate(spec);
+ * PrivateKey key = bikeAlgorithm.privateKeyImporter(BikePrivateKeySpec.class).importPrivate(spec);
*
* // Marshal for storage or transport
* PairSeq seq = BikePrivateKeySpec.marshal(spec);
@@ -60,10 +64,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* @since 1.0
*/
-public final class BikePrivateKeySpec implements AlgorithmKeySpec {
+public final class BikePrivateKeySpec implements AlgorithmKeySpec, Destroyable {
private static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] pkcs8;
+ private final ReentrantLock lifecycleLock = new ReentrantLock();
+ private boolean destroyed;
/**
* Constructs a new spec from a PKCS#8 encoded private key.
@@ -81,7 +87,13 @@ public final class BikePrivateKeySpec implements AlgorithmKeySpec {
* @return cloned PKCS#8 bytes
*/
public byte[] pkcs8() {
- return pkcs8.clone();
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return pkcs8.clone();
+ } finally {
+ lifecycleLock.unlock();
+ }
}
/**
@@ -99,7 +111,7 @@ public final class BikePrivateKeySpec implements AlgorithmKeySpec {
* @return serialized key representation
*/
public static PairSeq marshal(BikePrivateKeySpec spec) {
- String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
+ String b64 = spec.encodedKey();
return PairSeq.of("type", "BikePrivateKeySpec", PKCS8_B64, b64);
}
@@ -120,7 +132,12 @@ public final class BikePrivateKeySpec implements AlgorithmKeySpec {
if (b64 == null) {
throw new IllegalArgumentException("BikePrivateKeySpec: missing pkcs8.b64");
}
- return new BikePrivateKeySpec(Base64.getDecoder().decode(b64));
+ byte[] decoded = Base64.getDecoder().decode(b64);
+ try {
+ return new BikePrivateKeySpec(decoded);
+ } finally {
+ Arrays.fill(decoded, (byte) 0);
+ }
}
/**
@@ -132,4 +149,43 @@ public final class BikePrivateKeySpec implements AlgorithmKeySpec {
public String toString() {
return "BikePrivateKeySpec[len=" + pkcs8.length + "]";
}
+
+ private String encodedKey() {
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public void destroy() {
+ lifecycleLock.lock();
+ try {
+ if (!destroyed) {
+ Arrays.fill(pkcs8, (byte) 0);
+ destroyed = true;
+ }
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ lifecycleLock.lock();
+ try {
+ return destroyed;
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ private void ensureActive() {
+ if (destroyed) {
+ throw new IllegalStateException("BIKE private key specification has been destroyed");
+ }
+ }
}
diff --git a/lib/src/main/java/zeroecho/core/alg/bike/BikePublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/bike/BikePublicKeySpec.java
index fb52346..7b49dd2 100644
--- a/lib/src/main/java/zeroecho/core/alg/bike/BikePublicKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/bike/BikePublicKeySpec.java
@@ -49,7 +49,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* Usage
{@code
* // Import a BIKE public key
* BikePublicKeySpec spec = new BikePublicKeySpec(x509Bytes);
- * PublicKey key = bikeAlgorithm.importPublic(spec);
+ * PublicKey key = bikeAlgorithm.publicKeyImporter(BikePublicKeySpec.class).importPublic(spec);
*
* // Marshal for transport or storage
* PairSeq seq = BikePublicKeySpec.marshal(spec);
diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/AbstractChaChaAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/chacha/AbstractChaChaAlgorithm.java
index 30bd9e7..c2f72e7 100644
--- a/lib/src/main/java/zeroecho/core/alg/chacha/AbstractChaChaAlgorithm.java
+++ b/lib/src/main/java/zeroecho/core/alg/chacha/AbstractChaChaAlgorithm.java
@@ -35,13 +35,15 @@ package zeroecho.core.alg.chacha;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
+import java.util.Arrays;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import zeroecho.core.alg.AbstractCryptoAlgorithm;
-import zeroecho.core.spi.SymmetricKeyBuilder;
+import zeroecho.core.spi.SymmetricKeyGenerator;
+import zeroecho.core.spi.SymmetricKeyImporter;
/**
* Abstract base for ChaCha family algorithms
@@ -64,19 +66,20 @@ import zeroecho.core.spi.SymmetricKeyBuilder;
* {@code "ChaCha20"}.{@code
- * AbstractChaChaAlgorithm algo = ...;
+ * ZeroEchoSession session = new ZeroEchoSession();
*
* // Generate a fresh 256-bit key
- * SecretKey key = algo.generateSecret(ChaChaKeyGenSpec.chacha256());
+ * SecretKey key = session.keyBuilders().symmetric()
+ * .generate("ChaCha20", ChaChaKeyGenSpec.chacha256());
*
* // Import an existing key
- * SecretKey imported = algo.importSecret(new ChaChaKeyImportSpec(rawBytes));
+ * SecretKey imported = session.keyBuilders().symmetric()
+ * .importKey("ChaCha20", new ChaChaKeyImportSpec(rawBytes));
* }
*
* @since 1.0
@@ -93,30 +96,26 @@ abstract class AbstractChaChaAlgorithm extends AbstractCryptoAlgorithm {
super(id, title);
// register once for both algorithms (same 256-bit key)
- registerSymmetricKeyBuilder(ChaChaKeyGenSpec.class, new SymmetricKeyBuilder<>() {
+ registerSymmetricKeyGenerator(ChaChaKeyGenSpec.class, new SymmetricKeyGenerator<>() {
@Override
public SecretKey generateSecret(ChaChaKeyGenSpec spec) throws GeneralSecurityException {
KeyGenerator kg = KeyGenerator.getInstance("ChaCha20");
kg.init(spec.keySizeBits(), new SecureRandom());
return kg.generateKey();
}
-
- @Override
- public SecretKey importSecret(ChaChaKeyGenSpec spec) {
- throw new UnsupportedOperationException("Use ChaChaKeyImportSpec for importing ChaCha keys");
- }
}, ChaChaKeyGenSpec::chacha256);
- registerSymmetricKeyBuilder(ChaChaKeyImportSpec.class, new SymmetricKeyBuilder<>() {
- @Override
- public SecretKey generateSecret(ChaChaKeyImportSpec spec) {
- throw new UnsupportedOperationException("Use ChaChaKeyGenSpec to generate ChaCha keys");
- }
+ registerSymmetricKeyImporter(ChaChaKeyImportSpec.class, new SymmetricKeyImporter<>() {
@Override
public SecretKey importSecret(ChaChaKeyImportSpec spec) {
- return new SecretKeySpec(spec.key(), "ChaCha20");
+ byte[] key = spec.key();
+ try {
+ return new SecretKeySpec(key, "ChaCha20");
+ } finally {
+ Arrays.fill(key, (byte) 0);
+ }
}
- }, null);
+ });
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Algorithm.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Algorithm.java
index b75f2d0..ba07442 100644
--- a/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Algorithm.java
+++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Algorithm.java
@@ -74,19 +74,19 @@ import zeroecho.core.SymmetricHeaderCodec;
* corresponding cipher context.
*
* {@code
- * var algo = new ChaCha20Poly1305Algorithm();
- * SecretKey key = algo.generateSecret(ChaChaKeyGenSpec.chacha256());
+ * ZeroEchoSession session = new ZeroEchoSession();
+ * SecretKey key = session.keyBuilders().symmetric()
+ * .generate("CHACHA20-POLY1305", ChaChaKeyGenSpec.chacha256());
*
* // Encrypt with explicit spec
- * var spec = ChaCha20Poly1305Spec.builder().header(null).build();
- * EncryptionContext enc = algo.newContext(
- * zeroecho.core.AlgorithmFamily.SYMMETRIC,
- * zeroecho.core.KeyUsage.ENCRYPT, key, spec);
+ * ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(null).build();
+ * EncryptionContext enc = session.createContext(
+ * "CHACHA20-POLY1305", zeroecho.core.KeyUsage.ENCRYPT, key, spec);
*
* // Decrypt using VoidSpec default
- * EncryptionContext dec = algo.newContext(
- * zeroecho.core.AlgorithmFamily.SYMMETRIC,
- * zeroecho.core.KeyUsage.DECRYPT, key, zeroecho.core.spec.VoidSpec.INSTANCE);
+ * EncryptionContext dec = session.createContext(
+ * "CHACHA20-POLY1305", zeroecho.core.KeyUsage.DECRYPT, key,
+ * zeroecho.core.spec.VoidSpec.INSTANCE);
* }
*
* @since 1.0
diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaKeyImportSpec.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaKeyImportSpec.java
index e06c172..6e1f5a3 100644
--- a/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaKeyImportSpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaKeyImportSpec.java
@@ -38,6 +38,9 @@ import java.util.Arrays;
import java.util.Base64;
import java.util.HexFormat;
import java.util.Objects;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq;
import zeroecho.core.spec.AlgorithmKeySpec;
@@ -66,7 +69,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* {@code
* // Import from raw key bytes
* ChaChaKeyImportSpec spec = ChaChaKeyImportSpec.fromRaw(keyBytes);
- * SecretKey key = cryptoAlgorithm.importSecret(spec);
+ * SecretKey key = cryptoAlgorithm.symmetricKeyImporter(ChaChaKeyImportSpec.class).importSecret(spec);
*
* // Serialize to PairSeq
* PairSeq seq = ChaChaKeyImportSpec.marshal(spec);
@@ -77,8 +80,10 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* @since 1.0
*/
-public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
+public final class ChaChaKeyImportSpec implements AlgorithmKeySpec, Destroyable {
private final byte[] key;
+ private final ReentrantLock lifecycleLock = new ReentrantLock();
+ private boolean destroyed;
/**
* Creates a new import spec with the given raw key.
@@ -112,7 +117,12 @@ public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
* @return spec wrapping the decoded key
*/
public static ChaChaKeyImportSpec fromHex(String hex) {
- return fromRaw(HexFormat.of().parseHex(hex));
+ byte[] decoded = HexFormat.of().parseHex(hex);
+ try {
+ return fromRaw(decoded);
+ } finally {
+ Arrays.fill(decoded, (byte) 0);
+ }
}
/**
@@ -122,7 +132,12 @@ public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
* @return spec wrapping the decoded key
*/
public static ChaChaKeyImportSpec fromBase64(String b64) {
- return fromRaw(Base64.getDecoder().decode(b64));
+ byte[] decoded = Base64.getDecoder().decode(b64);
+ try {
+ return fromRaw(decoded);
+ } finally {
+ Arrays.fill(decoded, (byte) 0);
+ }
}
/**
@@ -131,7 +146,13 @@ public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
* @return 32-byte key array
*/
public byte[] key() {
- return Arrays.copyOf(key, key.length);
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return Arrays.copyOf(key, key.length);
+ } finally {
+ lifecycleLock.unlock();
+ }
}
/**
@@ -141,7 +162,7 @@ public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
* @return serialized key representation
*/
public static PairSeq marshal(ChaChaKeyImportSpec spec) {
- String k = Base64.getEncoder().withoutPadding().encodeToString(spec.key);
+ String k = spec.encodedKey();
return PairSeq.of("type", "CHACHA-KEY", "k.b64", k);
}
@@ -163,21 +184,81 @@ public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
*/
public static ChaChaKeyImportSpec unmarshal(PairSeq p) {
byte[] out = null;
- PairSeq.Cursor c = p.cursor();
- while (c.next()) {
- String k = c.key();
- String v = c.value();
- switch (k) {
- case "k.b64" -> out = Base64.getDecoder().decode(v);
- case "k.hex" -> out = HexFormat.of().parseHex(v);
- case "k.raw" -> out = v.getBytes(StandardCharsets.ISO_8859_1);
- default -> {
+ try {
+ PairSeq.Cursor c = p.cursor();
+ while (c.next()) {
+ String k = c.key();
+ String v = c.value();
+ switch (k) {
+ case "k.b64" -> {
+ wipe(out);
+ out = Base64.getDecoder().decode(v);
+ }
+ case "k.hex" -> {
+ wipe(out);
+ out = HexFormat.of().parseHex(v);
+ }
+ case "k.raw" -> {
+ wipe(out);
+ out = v.getBytes(StandardCharsets.ISO_8859_1);
+ }
+ default -> {
+ }
}
}
+ if (out == null) {
+ throw new IllegalArgumentException("ChaCha20 key missing (k.b64 / k.hex / k.raw)");
+ }
+ return new ChaChaKeyImportSpec(out);
+ } finally {
+ wipe(out);
}
- if (out == null) {
- throw new IllegalArgumentException("ChaCha20 key missing (k.b64 / k.hex / k.raw)");
+ }
+
+ private static void wipe(byte[] current) {
+ if (current != null) {
+ Arrays.fill(current, (byte) 0);
+ }
+ }
+
+ private String encodedKey() {
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return Base64.getEncoder().withoutPadding().encodeToString(key);
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void destroy() {
+ lifecycleLock.lock();
+ try {
+ if (!destroyed) {
+ Arrays.fill(key, (byte) 0);
+ destroyed = true;
+ }
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isDestroyed() {
+ lifecycleLock.lock();
+ try {
+ return destroyed;
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ private void ensureActive() {
+ if (destroyed) {
+ throw new IllegalStateException("ChaCha key import specification has been destroyed");
}
- return new ChaChaKeyImportSpec(out);
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/package-info.java b/lib/src/main/java/zeroecho/core/alg/chacha/package-info.java
index 1f2327e..680289b 100644
--- a/lib/src/main/java/zeroecho/core/alg/chacha/package-info.java
+++ b/lib/src/main/java/zeroecho/core/alg/chacha/package-info.java
@@ -37,9 +37,10 @@
*
* This package provides the ChaCha capability set for the core layer, including
* the stream cipher ChaCha20 and the AEAD construction ChaCha20-Poly1305. The
- * module contains algorithm descriptors, streaming cipher contexts, immutable
- * specifications, optional header codecs for runtime parameters, and symmetric
- * key import/generation specifications. The design favors safe defaults
+ * module contains algorithm descriptors, streaming cipher contexts,
+ * configuration specifications, optional header codecs for runtime parameters,
+ * and symmetric key import/generation specifications. Key import
+ * specifications are destroyable. The design favors safe defaults
* (12-byte nonces, 128-bit AEAD tag), explicit role-to-context binding, and a
* clear separation between static configuration and per-operation parameters.
*
diff --git a/lib/src/main/java/zeroecho/core/alg/cmce/CmceAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/cmce/CmceAlgorithm.java
index 84d5475..686bb09 100644
--- a/lib/src/main/java/zeroecho/core/alg/cmce/CmceAlgorithm.java
+++ b/lib/src/main/java/zeroecho/core/alg/cmce/CmceAlgorithm.java
@@ -44,6 +44,7 @@ import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Arrays;
import java.security.spec.X509EncodedKeySpec;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
@@ -56,7 +57,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter;
import zeroecho.core.context.KemContext;
import zeroecho.core.context.MessageAgreementContext;
import zeroecho.core.spec.VoidSpec;
-import zeroecho.core.spi.AsymmetricKeyBuilder;
+import zeroecho.core.spi.AsymmetricKeyPairGenerator;
+import zeroecho.core.spi.PrivateKeyImporter;
+import zeroecho.core.spi.PublicKeyImporter;
/**
* Classic McEliece (CMCE) algorithm adapter
@@ -106,15 +109,15 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* CmceAlgorithm alg = new CmceAlgorithm();
*
* // Generate a key pair with a chosen CMCE variant.
- * KeyPair kp = alg.asymmetricKeyBuilder(CmceKeyGenSpec.class)
+ * KeyPair kp = alg.asymmetricKeyPairGenerator(CmceKeyGenSpec.class)
* .generateKeyPair(CmceKeyGenSpec.mceliece8192128f());
*
* // Create a KEM encapsulation context with the recipient public key.
- * KemContext enc = alg.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
+ * KemContext enc = alg.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
*
* // Create an agreement initiator context backed by CMCE KEM.
* MessageAgreementContext initiator =
- * alg.create(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE);
+ * alg.createContext(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE);
* }
*
* @since 1.0
@@ -169,7 +172,7 @@ public final class CmceAlgorithm extends AbstractCryptoAlgorithm {
.build();
}, () -> VoidSpec.INSTANCE);
- registerAsymmetricKeyBuilder(CmceKeyGenSpec.class, new AsymmetricKeyBuilder<>() {
+ registerAsymmetricKeyPairGenerator(CmceKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
@Override
public KeyPair generateKeyPair(CmceKeyGenSpec spec) throws GeneralSecurityException {
ensureProvider();
@@ -189,23 +192,9 @@ public final class CmceAlgorithm extends AbstractCryptoAlgorithm {
kpg.initialize(params, new SecureRandom());
return kpg.generateKeyPair();
}
-
- @Override
- public PublicKey importPublic(CmceKeyGenSpec spec) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public PrivateKey importPrivate(CmceKeyGenSpec spec) {
- throw new UnsupportedOperationException();
- }
}, CmceKeyGenSpec::mceliece8192128f);
- registerAsymmetricKeyBuilder(CmcePublicKeySpec.class, new AsymmetricKeyBuilder<>() {
- @Override
- public KeyPair generateKeyPair(CmcePublicKeySpec spec) {
- throw new UnsupportedOperationException();
- }
+ registerPublicKeyImporter(CmcePublicKeySpec.class, new PublicKeyImporter<>() {
@Override
public PublicKey importPublic(CmcePublicKeySpec spec) throws GeneralSecurityException {
@@ -213,31 +202,22 @@ public final class CmceAlgorithm extends AbstractCryptoAlgorithm {
KeyFactory kf = KeyFactory.getInstance("CMCE", providerName());
return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
}
+ });
- @Override
- public PrivateKey importPrivate(CmcePublicKeySpec spec) {
- throw new UnsupportedOperationException();
- }
- }, null);
-
- registerAsymmetricKeyBuilder(CmcePrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
- @Override
- public KeyPair generateKeyPair(CmcePrivateKeySpec spec) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public PublicKey importPublic(CmcePrivateKeySpec spec) {
- throw new UnsupportedOperationException();
- }
+ registerPrivateKeyImporter(CmcePrivateKeySpec.class, new PrivateKeyImporter<>() {
@Override
public PrivateKey importPrivate(CmcePrivateKeySpec spec) throws GeneralSecurityException {
ensureProvider();
KeyFactory kf = KeyFactory.getInstance("CMCE", providerName());
- return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8()));
+ byte[] encoded = spec.pkcs8();
+ try {
+ return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
+ } finally {
+ Arrays.fill(encoded, (byte) 0);
+ }
}
- }, null);
+ });
}
private static void ensureProvider() throws NoSuchProviderException {
diff --git a/lib/src/main/java/zeroecho/core/alg/cmce/CmceKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/cmce/CmceKeyGenSpec.java
index 6fcd143..a54f936 100644
--- a/lib/src/main/java/zeroecho/core/alg/cmce/CmceKeyGenSpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/cmce/CmceKeyGenSpec.java
@@ -52,7 +52,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* {@code
* // Generate a key pair for McEliece 8192128F (256-bit security, fast)
* CmceKeyGenSpec spec = CmceKeyGenSpec.mceliece8192128f();
- * KeyPair kp = alg.asymmetricKeyBuilder(CmceKeyGenSpec.class).generateKeyPair(spec);
+ * KeyPair kp = alg.asymmetricKeyPairGenerator(CmceKeyGenSpec.class).generateKeyPair(spec);
* }
*
* @since 1.0
diff --git a/lib/src/main/java/zeroecho/core/alg/cmce/CmcePrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/cmce/CmcePrivateKeySpec.java
index 0c70a52..f824d1e 100644
--- a/lib/src/main/java/zeroecho/core/alg/cmce/CmcePrivateKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/cmce/CmcePrivateKeySpec.java
@@ -33,8 +33,12 @@
******************************************************************************/
package zeroecho.core.alg.cmce;
+import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq;
import zeroecho.core.marshal.PairSeq.Cursor;
@@ -49,8 +53,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
*
* - * Instances are immutable. The internal byte array is cloned on construction - * and on every accessor to prevent accidental mutation. + * The internal byte array is cloned on construction and on every accessor. + * Access and destruction are synchronized. *
* *- * Internally this delegates to the JCA {@link KeyAgreement} API with the given + * Internally this delegates to the JCA key-agreement API with the given * {@code jcaName} and optional provider. *
* @@ -152,18 +144,7 @@ public class GenericJcaAgreementContext implements AgreementContext { */ @Override public byte[] deriveSecret() { - if (peer == null) { - throw new IllegalStateException("Peer public key not set"); - } - try { - KeyAgreement ka = (provider == null) ? KeyAgreement.getInstance(jcaName) - : KeyAgreement.getInstance(jcaName, provider); - ka.init(privateKey); - ka.doPhase(peer, true); - return ka.generateSecret(); - } catch (GeneralSecurityException e) { - throw new IllegalArgumentException("KeyAgreement failed for " + jcaName, e); - } + return engine.deriveSecret(); } /** diff --git a/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaMessageAgreementContext.java b/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaMessageAgreementContext.java index d9072e3..3cee45a 100644 --- a/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaMessageAgreementContext.java +++ b/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaMessageAgreementContext.java @@ -104,9 +104,9 @@ import zeroecho.core.context.MessageAgreementContext; * * @since 1.0 */ -public final class GenericJcaMessageAgreementContext extends GenericJcaAgreementContext - implements MessageAgreementContext { +public final class GenericJcaMessageAgreementContext implements MessageAgreementContext { + private final GenericJcaAgreementContext agreement; private final PublicKey localPublic; private final String keyFactoryAlg; private final String keyFactoryProvider; @@ -139,7 +139,8 @@ public final class GenericJcaMessageAgreementContext extends GenericJcaAgreement */ public GenericJcaMessageAgreementContext(CryptoAlgorithm alg, KeyPairKey keyPairKey, String jcaAgreementName, String agreementProvider, String keyFactoryAlg, String keyFactoryProvider) { - super(Objects.requireNonNull(alg, "alg"), Objects.requireNonNull(keyPairKey, "keyPairKey").privateKey(), + this.agreement = new GenericJcaAgreementContext(Objects.requireNonNull(alg, "alg"), + Objects.requireNonNull(keyPairKey, "keyPairKey").privateKey(), Objects.requireNonNull(jcaAgreementName, "jcaAgreementName"), agreementProvider); this.localPublic = Objects.requireNonNull(keyPairKey.publicKey(), "keyPairKey.public"); this.keyFactoryAlg = Objects.requireNonNull(keyFactoryAlg, "keyFactoryAlg"); @@ -199,12 +200,18 @@ public final class GenericJcaMessageAgreementContext extends GenericJcaAgreement @Override public void setPeerMessage(byte[] message) { if (message == null) { - setPeerPublic(null); + agreement.setPeerPublic(null); return; } PublicKey peerPublic = importPeerPublic(message); - setPeerPublic(peerPublic); + agreement.setPeerPublic(peerPublic); + } + + /** {@inheritDoc} */ + @Override + public void setPeerPublic(PublicKey peer) { + agreement.setPeerPublic(peer); } /** @@ -232,4 +239,28 @@ public final class GenericJcaMessageAgreementContext extends GenericJcaAgreement throw new IllegalArgumentException("Failed to import peer public key using KeyFactory " + keyFactoryAlg, e); } } + + /** {@inheritDoc} */ + @Override + public byte[] deriveSecret() { + return agreement.deriveSecret(); + } + + /** {@inheritDoc} */ + @Override + public CryptoAlgorithm algorithm() { + return agreement.algorithm(); + } + + /** {@inheritDoc} */ + @Override + public java.security.Key key() { + return agreement.key(); + } + + /** {@inheritDoc} */ + @Override + public void close() { + agreement.close(); + } } diff --git a/lib/src/main/java/zeroecho/core/alg/common/agreement/JcaAgreementEngine.java b/lib/src/main/java/zeroecho/core/alg/common/agreement/JcaAgreementEngine.java new file mode 100644 index 0000000..6271420 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/common/agreement/JcaAgreementEngine.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (C) 2026, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the conditions in the project LICENSE are met. + ******************************************************************************/ +package zeroecho.core.alg.common.agreement; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Objects; + +import javax.crypto.KeyAgreement; + +/** + * Package-private reusable JCA agreement mechanics. + */ +final class JcaAgreementEngine { + private final PrivateKey privateKey; + private final String jcaName; + private final String provider; + private PublicKey peer; + + /* default */ JcaAgreementEngine(PrivateKey privateKey, String jcaName, String provider) { + this.privateKey = Objects.requireNonNull(privateKey, "privateKey must not be null"); + this.jcaName = Objects.requireNonNull(jcaName, "jcaName must not be null"); + this.provider = provider; + } + + /* default */ PrivateKey privateKey() { + return privateKey; + } + + /* default */ void setPeerPublic(PublicKey peer) { + this.peer = peer; + } + + /* default */ byte[] deriveSecret() { + if (peer == null) { + throw new IllegalStateException("Peer public key not set"); + } + try { + KeyAgreement agreement = provider == null ? KeyAgreement.getInstance(jcaName) + : KeyAgreement.getInstance(jcaName, provider); + agreement.init(privateKey); + agreement.doPhase(peer, true); + return agreement.generateSecret(); + } catch (GeneralSecurityException exception) { + throw new IllegalArgumentException("KeyAgreement failed for " + jcaName, exception); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEdDSAKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEdDSAKeyGenBuilder.java index 4659009..57b5130 100644 --- a/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEdDSAKeyGenBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEdDSAKeyGenBuilder.java @@ -36,11 +36,9 @@ package zeroecho.core.alg.common.eddsa; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyPairGenerator; -import java.security.PrivateKey; -import java.security.PublicKey; import zeroecho.core.spec.AlgorithmKeySpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; /** *- * The provided {@code spec} is not inspected in this base implementation, but - * it satisfies the {@link AsymmetricKeyBuilder} contract. Subclasses may extend - * this behavior to interpret spec parameters. + * The provided {@code spec} is not inspected in this base implementation. + * Subclasses may extend this behavior to interpret specification parameters. *
* * @param spec algorithm-specific key specification (currently unused) @@ -107,38 +104,4 @@ public abstract class AbstractEdDSAKeyGenBuilder- * Importing encoded EdDSA public keys must be done through the corresponding - * {@code *PublicKeySpec} builder class. - *
- * - * @param spec algorithm-specific key specification - * @return never returns normally - * @throws UnsupportedOperationException always thrown - */ - @Override - public PublicKey importPublic(S spec) { - throw new UnsupportedOperationException("Use the corresponding PublicKeySpec to import a public key."); - } - - /** - * Always throws, as this builder does not support private key import. - * - *- * Importing encoded EdDSA private keys must be done through the corresponding - * {@code *PrivateKeySpec} builder class. - *
- * - * @param spec algorithm-specific key specification - * @return never returns normally - * @throws UnsupportedOperationException always thrown - */ - @Override - public PrivateKey importPrivate(S spec) { - throw new UnsupportedOperationException("Use the corresponding PrivateKeySpec to import a private key."); - } } diff --git a/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEncodedPrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEncodedPrivateKeyBuilder.java index b2114d4..bb2f06e 100644 --- a/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEncodedPrivateKeyBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEncodedPrivateKeyBuilder.java @@ -35,13 +35,12 @@ package zeroecho.core.alg.common.eddsa; import java.security.GeneralSecurityException; import java.security.KeyFactory; -import java.security.KeyPair; import java.security.PrivateKey; -import java.security.PublicKey; import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; import zeroecho.core.spec.AlgorithmKeySpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.PrivateKeyImporter; /** *- * Raw public key import is not implemented because this builder focuses on key - * generation from DH parameters. Use higher-level catalog or codec facilities - * to parse or construct {@link PublicKey} instances if needed. - *
- * - *- * Example - *
- *{@code
- * // This will throw UnsupportedOperationException
- * new DhKeyGenBuilder().importPublic(DhSpec.ffdhe2048());
- * }
- *
- * @param spec the DH specification (ignored)
- * @return never returns normally
- * @throws UnsupportedOperationException always thrown
- */
- @Override
- public PublicKey importPublic(DhSpec spec) {
- throw new UnsupportedOperationException();
- }
-
- /**
- * Unsupported for DH in this builder.
- *
- * - * Raw private key import is not implemented because this builder focuses on key - * generation from DH parameters. Use higher-level catalog or codec facilities - * to parse or construct {@link PrivateKey} instances if needed. - *
- * - *- * Example - *
- *{@code
- * // This will throw UnsupportedOperationException
- * new DhKeyGenBuilder().importPrivate(DhSpec.ffdhe2048());
- * }
- *
- * @param spec the DH specification (ignored)
- * @return never returns normally
- * @throws UnsupportedOperationException always thrown
- */
- @Override
- public PrivateKey importPrivate(DhSpec spec) {
- throw new UnsupportedOperationException();
- }
}
diff --git a/lib/src/main/java/zeroecho/core/alg/dh/DhPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/dh/DhPrivateKeySpec.java
index b917dc3..03fdd75 100644
--- a/lib/src/main/java/zeroecho/core/alg/dh/DhPrivateKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/dh/DhPrivateKeySpec.java
@@ -33,7 +33,11 @@
******************************************************************************/
package zeroecho.core.alg.dh;
+import java.util.Arrays;
import java.util.Base64;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq;
import zeroecho.core.spec.AlgorithmKeySpec;
@@ -50,8 +54,9 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* {@code
* // Import a DH private key from encoded bytes
* DhPrivateKeySpec spec = new DhPrivateKeySpec(pkcs8Bytes);
- * PrivateKey priv = CryptoAlgorithms.privateKey("DH", spec);
+ * PrivateKey priv = session.keyBuilders().asymmetric().importPrivate("DH", spec);
*
* // Marshal to a text-friendly representation
* PairSeq ps = DhPrivateKeySpec.marshal(spec);
@@ -73,10 +78,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* @since 1.0
*/
-public class DhPrivateKeySpec implements AlgorithmKeySpec {
+public class DhPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
private static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] pkcs8;
+ private final ReentrantLock lifecycleLock = new ReentrantLock();
+ private boolean destroyed;
/**
* Creates a new specification from a PKCS#8 encoded DH private key.
@@ -97,7 +104,13 @@ public class DhPrivateKeySpec implements AlgorithmKeySpec {
* @return a defensive copy of the PKCS#8 encoded DH private key
*/
public byte[] encoded() {
- return pkcs8.clone();
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return pkcs8.clone();
+ } finally {
+ lifecycleLock.unlock();
+ }
}
/**
@@ -113,7 +126,7 @@ public class DhPrivateKeySpec implements AlgorithmKeySpec {
* @throws NullPointerException if {@code spec} is {@code null}
*/
public static PairSeq marshal(DhPrivateKeySpec spec) {
- String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
+ String b64 = spec.encodedKey();
return PairSeq.of("type", "DH-PRIV", PKCS8_B64, b64);
}
@@ -137,12 +150,62 @@ public class DhPrivateKeySpec implements AlgorithmKeySpec {
String k = cur.key();
String v = cur.value();
if (PKCS8_B64.equals(k)) {
- out = Base64.getDecoder().decode(v);
+ out = decodeReplacing(out, v);
}
}
if (out == null) {
throw new IllegalArgumentException("pkcs8.b64 missing for DH private key");
}
- return new DhPrivateKeySpec(out);
+ try {
+ return new DhPrivateKeySpec(out);
+ } finally {
+ Arrays.fill(out, (byte) 0);
+ }
+ }
+
+ private static byte[] decodeReplacing(byte[] current, String encoded) {
+ if (current != null) {
+ Arrays.fill(current, (byte) 0);
+ }
+ return Base64.getDecoder().decode(encoded);
+ }
+
+ private String encodedKey() {
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public void destroy() {
+ lifecycleLock.lock();
+ try {
+ if (!destroyed) {
+ Arrays.fill(pkcs8, (byte) 0);
+ destroyed = true;
+ }
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ lifecycleLock.lock();
+ try {
+ return destroyed;
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ private void ensureActive() {
+ if (destroyed) {
+ throw new IllegalStateException("DH private key specification has been destroyed");
+ }
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/dh/DhPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/dh/DhPublicKeySpec.java
index 5698124..6ed5966 100644
--- a/lib/src/main/java/zeroecho/core/alg/dh/DhPublicKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/dh/DhPublicKeySpec.java
@@ -64,7 +64,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* Example
{@code
* // Import a DH public key from encoded bytes
* DhPublicKeySpec spec = new DhPublicKeySpec(x509Bytes);
- * PublicKey pub = CryptoAlgorithms.publicKey("DH", spec);
+ * PublicKey pub = session.keyBuilders().asymmetric().importPublic("DH", spec);
*
* // Marshal to a text-friendly representation
* PairSeq ps = DhPublicKeySpec.marshal(spec);
diff --git a/lib/src/main/java/zeroecho/core/alg/dh/DhSpec.java b/lib/src/main/java/zeroecho/core/alg/dh/DhSpec.java
index 0610c6f..7d9ff2f 100644
--- a/lib/src/main/java/zeroecho/core/alg/dh/DhSpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/dh/DhSpec.java
@@ -80,10 +80,10 @@ import zeroecho.core.spec.ContextSpec;
*
* Example
{@code
* // Create a key pair in the FFDHE-3072 group
- * KeyPair kp = CryptoAlgorithms.keyPair("DH", DhSpec.ffdhe3072());
+ * KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("DH", DhSpec.ffdhe3072());
*
* // Establish an agreement context
- * AgreementContext ctx = CryptoAlgorithms.create(
+ * AgreementContext ctx = session.createContext(
* "DH", KeyUsage.AGREEMENT, kp.getPrivate(), DhSpec.ffdhe3072());
* }
*
diff --git a/lib/src/main/java/zeroecho/core/alg/dh/package-info.java b/lib/src/main/java/zeroecho/core/alg/dh/package-info.java
index 0cfe37e..60695dc 100644
--- a/lib/src/main/java/zeroecho/core/alg/dh/package-info.java
+++ b/lib/src/main/java/zeroecho/core/alg/dh/package-info.java
@@ -51,8 +51,8 @@
* ad-hoc parameter generation.
* {@code
* // Generate a key pair for Alice
- * KeyPair aliceKeys = CryptoAlgorithms.generateKeyPair("ECDH", EcdhCurveSpec.P256);
+ * KeyPair aliceKeys = session.keyBuilders().asymmetric().generateKeyPair("ECDH", EcdhCurveSpec.P256);
*
* // Generate a key pair for Bob
- * KeyPair bobKeys = CryptoAlgorithms.generateKeyPair("ECDH", EcdhCurveSpec.P256);
+ * KeyPair bobKeys = session.keyBuilders().asymmetric().generateKeyPair("ECDH", EcdhCurveSpec.P256);
*
* // Alice computes shared secret using her private key
- * AgreementContext aliceCtx = CryptoAlgorithms.create("ECDH",
+ * AgreementContext aliceCtx = session.createContext("ECDH",
* KeyUsage.AGREEMENT, aliceKeys.getPrivate(), EcdsaCurveSpec.P256);
* byte[] aliceSecret = aliceCtx.derive(bobKeys.getPublic());
*
* // Bob computes shared secret using his private key
- * AgreementContext bobCtx = CryptoAlgorithms.create("ECDH",
+ * AgreementContext bobCtx = session.createContext("ECDH",
* KeyUsage.AGREEMENT, bobKeys.getPrivate(), EcdsaCurveSpec.P256);
* byte[] bobSecret = bobCtx.derive(aliceKeys.getPublic());
*
@@ -160,8 +160,9 @@ public final class EcdhAlgorithm extends AbstractCryptoAlgorithm {
() -> EcdsaCurveSpec.P256);
// Reuse EC builders/importers
- registerAsymmetricKeyBuilder(EcdhCurveSpec.class, new EcdhKeyGenBuilder(), () -> EcdhCurveSpec.P256);
- registerAsymmetricKeyBuilder(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder(), null);
- registerAsymmetricKeyBuilder(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder(), null);
+ registerAsymmetricKeyPairGenerator(EcdhCurveSpec.class, new EcdhKeyGenBuilder(),
+ () -> EcdhCurveSpec.P256);
+ registerPublicKeyImporter(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder());
+ registerPrivateKeyImporter(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder());
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhCurveSpec.java b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhCurveSpec.java
index e6179aa..bb2e9a9 100644
--- a/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhCurveSpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhCurveSpec.java
@@ -63,7 +63,7 @@ import zeroecho.core.spec.ContextSpec;
*
* Usage
{@code
* // Generate a key pair on P-256
- * KeyPair kp = CryptoAlgorithms.generateKeyPair("ECDH", EcdhCurveSpec.P256);
+ * KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("ECDH", EcdhCurveSpec.P256);
*
* // Use curve metadata
* String jcaName = EcdhCurveSpec.P256.curveName(); // "secp256r1"
diff --git a/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhKeyGenBuilder.java
index df79109..451ac69 100644
--- a/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhKeyGenBuilder.java
+++ b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhKeyGenBuilder.java
@@ -39,15 +39,13 @@ import java.security.KeyPairGenerator;
import java.security.spec.ECGenParameterSpec;
import zeroecho.core.alg.ecdsa.EcdsaPrivateKeyBuilder;
-import zeroecho.core.alg.ecdsa.EcdsaPrivateKeySpec;
import zeroecho.core.alg.ecdsa.EcdsaPublicKeyBuilder;
-import zeroecho.core.alg.ecdsa.EcdsaPublicKeySpec;
-import zeroecho.core.spi.AsymmetricKeyBuilder;
+import zeroecho.core.spi.AsymmetricKeyPairGenerator;
/**
* ECDH Key Pair Generator
*
- * Implementation of {@link AsymmetricKeyBuilder} for elliptic curve
+ * Implementation of {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} for elliptic curve
* Diffie-Hellman (ECDH) key pairs.
*
*
@@ -70,7 +68,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
*
* @since 1.0
*/
-public final class EcdhKeyGenBuilder implements AsymmetricKeyBuilder {
+public final class EcdhKeyGenBuilder implements AsymmetricKeyPairGenerator {
/**
* Generates a new elliptic curve key pair for use in ECDH key agreement.
*
@@ -92,40 +90,4 @@ public final class EcdhKeyGenBuilder implements AsymmetricKeyBuilder
- * Importing existing ECDH public keys should be performed via
- * {@link EcdsaPublicKeyBuilder} with an {@link EcdsaPublicKeySpec}. This method
- * will always throw an {@link UnsupportedOperationException}.
- *
- *
- * @param spec ignored
- * @return never returns normally
- * @throws UnsupportedOperationException always
- */
- @Override
- public java.security.PublicKey importPublic(EcdhCurveSpec spec) {
- throw new UnsupportedOperationException("Use EcdhPublicKeySpec with EcdsaPublicKeyBuilder.");
- }
-
- /**
- * Unsupported operation for this builder.
- *
- *
- * Importing existing ECDH private keys should be performed via
- * {@link EcdsaPrivateKeyBuilder} with an {@link EcdsaPrivateKeySpec}. This
- * method will always throw an {@link UnsupportedOperationException}.
- *
- *
- * @param spec ignored
- * @return never returns normally
- * @throws UnsupportedOperationException always
- */
- @Override
- public java.security.PrivateKey importPrivate(EcdhCurveSpec spec) {
- throw new UnsupportedOperationException("Use EcdhPrivateKeySpec with EcdsaPrivateKeyBuilder.");
- }
}
diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaAlgorithm.java
index 6c7a1df..797630f 100644
--- a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaAlgorithm.java
+++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaAlgorithm.java
@@ -39,7 +39,6 @@ import java.security.PublicKey;
import zeroecho.core.AlgorithmFamily;
import zeroecho.core.CryptoAlgorithm;
-import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.CryptoCatalog;
import zeroecho.core.KeyUsage;
import zeroecho.core.alg.AbstractCryptoAlgorithm;
@@ -82,9 +81,9 @@ import zeroecho.core.context.SignatureContext;
*
* {@code
* // Example: Sign and verify with ECDSA/P-256
- * KeyPair kp = CryptoAlgorithms.keyPair("ECDSA", EcdsaCurveSpec.P256);
- * SignatureContext signer = CryptoAlgorithms.create("ECDSA", KeyUsage.SIGN, kp.getPrivate(), EcdsaCurveSpec.P256);
- * SignatureContext verifier = CryptoAlgorithms.create("ECDSA", KeyUsage.VERIFY, kp.getPublic(), EcdsaCurveSpec.P256);
+ * KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("ECDSA", EcdsaCurveSpec.P256);
+ * SignatureContext signer = session.createContext("ECDSA", KeyUsage.SIGN, kp.getPrivate(), EcdsaCurveSpec.P256);
+ * SignatureContext verifier = session.createContext("ECDSA", KeyUsage.VERIFY, kp.getPublic(), EcdsaCurveSpec.P256);
* }
*
* @since 1.0
@@ -104,8 +103,8 @@ public final class EcdsaAlgorithm extends AbstractCryptoAlgorithm {
*
* On construction, the algorithm declares its supported roles and registers
* builders with the {@link CryptoAlgorithm} infrastructure so they can be
- * discovered by the {@link CryptoCatalog} or invoked through
- * {@link CryptoAlgorithms} convenience methods.
+ * discovered by the {@link CryptoCatalog} or invoked through the
+ * session-bound {@link zeroecho.sdk.KeyBuilders} entry point.
*
*/
public EcdsaAlgorithm() {
@@ -135,8 +134,9 @@ public final class EcdsaAlgorithm extends AbstractCryptoAlgorithm {
}
}, () -> EcdsaCurveSpec.P256);
- registerAsymmetricKeyBuilder(EcdsaCurveSpec.class, new EcdsaKeyGenBuilder(), () -> EcdsaCurveSpec.P256);
- registerAsymmetricKeyBuilder(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder(), null);
- registerAsymmetricKeyBuilder(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder(), null);
+ registerAsymmetricKeyPairGenerator(EcdsaCurveSpec.class, new EcdsaKeyGenBuilder(),
+ () -> EcdsaCurveSpec.P256);
+ registerPublicKeyImporter(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder());
+ registerPrivateKeyImporter(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder());
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaKeyGenBuilder.java
index c759fb7..7f628b5 100644
--- a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaKeyGenBuilder.java
+++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaKeyGenBuilder.java
@@ -39,30 +39,23 @@ import java.security.KeyPairGenerator;
import java.security.spec.ECGenParameterSpec;
import zeroecho.core.CryptoAlgorithm;
-import zeroecho.core.CryptoAlgorithms;
-import zeroecho.core.spi.AsymmetricKeyBuilder;
+import zeroecho.core.spi.AsymmetricKeyPairGenerator;
/**
* ECDSA Key Pair Generator
*
- * Implementation of {@link AsymmetricKeyBuilder} for {@link EcdsaCurveSpec}.
+ * Implementation of {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} for
+ * {@link EcdsaCurveSpec}.
* This builder is responsible for generating new elliptic curve key pairs for
* use with the {@link EcdsaAlgorithm}.
*
- * Supported operations
- * The exact supported operation is + * {@link #generateKeyPair(EcdsaCurveSpec)}. Public and private import are + * registered separately through {@link EcdsaPublicKeyBuilder} and + * {@link EcdsaPrivateKeyBuilder}.
* - *{@code
* // Example: Generate an ECDSA P-256 key pair
@@ -72,7 +65,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
*
* @since 1.0
*/
-public final class EcdsaKeyGenBuilder implements AsymmetricKeyBuilder {
+public final class EcdsaKeyGenBuilder implements AsymmetricKeyPairGenerator {
/**
* Generates a new elliptic curve key pair for the given curve specification.
*
@@ -93,38 +86,4 @@ public final class EcdsaKeyGenBuilder implements AsymmetricKeyBuilder
- * Public key import should be performed using {@link EcdsaPublicKeySpec} and
- * {@link EcdsaPublicKeyBuilder}.
- *
- *
- * @param spec unused curve specification
- * @return never returns normally
- * @throws UnsupportedOperationException always thrown
- */
- @Override
- public java.security.PublicKey importPublic(EcdsaCurveSpec spec) {
- throw new UnsupportedOperationException("Use EcdsaPublicKeySpec with EcdsaPublicKeyBuilder.");
- }
-
- /**
- * Unsupported operation for this builder.
- *
- *
- * Private key import should be performed using {@link EcdsaPrivateKeySpec} and
- * {@link EcdsaPrivateKeyBuilder}.
- *
- *
- * @param spec unused curve specification
- * @return never returns normally
- * @throws UnsupportedOperationException always thrown
- */
- @Override
- public java.security.PrivateKey importPrivate(EcdsaCurveSpec spec) {
- throw new UnsupportedOperationException("Use EcdsaPrivateKeySpec with EcdsaPrivateKeyBuilder.");
- }
}
diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPrivateKeyBuilder.java
index a9e76b8..ac05027 100644
--- a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPrivateKeyBuilder.java
+++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPrivateKeyBuilder.java
@@ -37,36 +37,28 @@ import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Arrays;
import zeroecho.core.CryptoAlgorithm;
-import zeroecho.core.CryptoAlgorithms;
-import zeroecho.core.spi.AsymmetricKeyBuilder;
+import zeroecho.core.spi.PrivateKeyImporter;
/**
* ECDSA Private Key Builder
*
- * Implementation of {@link AsymmetricKeyBuilder} for
+ * Implementation of {@link zeroecho.core.spi.PrivateKeyImporter} for
* {@link EcdsaPrivateKeySpec}. This builder is responsible for importing ECDSA
* private keys from encoded representations.
*
- * Supported operations
- *
- * - {@link #importPrivate(EcdsaPrivateKeySpec)} - construct a
- * {@link PrivateKey} instance from a PKCS#8 encoded key.
- * - {@link #generateKeyPair(EcdsaPrivateKeySpec)} - unsupported; use
- * {@link EcdsaKeyGenBuilder} instead.
- * - {@link #importPublic(EcdsaPrivateKeySpec)} - unsupported; use
- * {@link EcdsaPublicKeySpec} with {@link EcdsaPublicKeyBuilder} instead.
- *
+ * The exact supported operation is
+ * {@link #importPrivate(EcdsaPrivateKeySpec)}. Generation and public import are
+ * registered through their own operation-specific implementations.
*
* Encoding
The {@link EcdsaPrivateKeySpec} stores the private key in
* PKCS#8 DER format. This builder delegates to a JCA {@link KeyFactory} for the
* {@code "EC"} algorithm to reconstruct a usable {@link PrivateKey}.
*
- * Usage
Typically accessed indirectly through
- * {@link CryptoAlgorithms#privateKey(String, zeroecho.core.spec.AlgorithmKeySpec)}
- * or
- * {@link CryptoAlgorithm#importPrivate(zeroecho.core.spec.AlgorithmKeySpec)}.
+ * Usage
Typically accessed through the session key-operation API or
+ * {@link CryptoAlgorithm#privateKeyImporter(Class)}.
*
* {@code
* // Example: Import an ECDSA private key
@@ -77,40 +69,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
*
* @since 1.0
*/
-public final class EcdsaPrivateKeyBuilder implements AsymmetricKeyBuilder {
- /**
- * Unsupported operation for this builder.
- *
- *
- * ECDSA key pair generation should be performed using
- * {@link EcdsaKeyGenBuilder}, not from a private key specification.
- *
- *
- * @param spec unused private key specification
- * @return never returns normally
- * @throws UnsupportedOperationException always thrown
- */
- @Override
- public java.security.KeyPair generateKeyPair(EcdsaPrivateKeySpec spec) {
- throw new UnsupportedOperationException("Use EcdsaKeyGenBuilder for keypair generation.");
- }
-
- /**
- * Unsupported operation for this builder.
- *
- *
- * Public key import should be performed using {@link EcdsaPublicKeySpec} with
- * {@link EcdsaPublicKeyBuilder}.
- *
- *
- * @param spec unused private key specification
- * @return never returns normally
- * @throws UnsupportedOperationException always thrown
- */
- @Override
- public java.security.PublicKey importPublic(EcdsaPrivateKeySpec spec) {
- throw new UnsupportedOperationException("Use EcdsaPublicKeySpec with EcdsaPublicKeyBuilder.");
- }
+public final class EcdsaPrivateKeyBuilder implements PrivateKeyImporter {
/**
* Imports a private key from a PKCS#8 encoded specification.
@@ -128,6 +87,11 @@ public final class EcdsaPrivateKeyBuilder implements AsymmetricKeyBuilderECDSA Private Key Specification
*
- * An immutable wrapper around a PKCS#8-encoded ECDSA private key. This
+ * A destroyable wrapper around a PKCS#8-encoded ECDSA private key. This
* specification is used by {@link EcdsaPrivateKeyBuilder} to import keys into
* the JCA {@link java.security.PrivateKey} representation.
*
@@ -69,10 +73,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* @since 1.0
*/
-public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec {
+public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
private static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] pkcs8;
+ private final ReentrantLock lifecycleLock = new ReentrantLock();
+ private boolean destroyed;
/**
* Creates a new private key specification from a PKCS#8 encoded byte array.
@@ -93,7 +99,13 @@ public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec {
* @return cloned PKCS#8 byte array
*/
public byte[] encoded() {
- return pkcs8.clone();
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return pkcs8.clone();
+ } finally {
+ lifecycleLock.unlock();
+ }
}
/**
@@ -108,7 +120,7 @@ public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec {
* @return serialized representation in key-value form
*/
public static PairSeq marshal(EcdsaPrivateKeySpec spec) {
- String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
+ String b64 = spec.encodedKey();
return PairSeq.of("type", "ECDSA-PRIV", PKCS8_B64, b64);
}
@@ -131,12 +143,62 @@ public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec {
String k = cur.key();
String v = cur.value();
if (PKCS8_B64.equals(k)) {
- out = Base64.getDecoder().decode(v);
+ out = decodeReplacing(out, v);
}
}
if (out == null) {
throw new IllegalArgumentException("pkcs8.b64 missing for ECDSA private key");
}
- return new EcdsaPrivateKeySpec(out);
+ try {
+ return new EcdsaPrivateKeySpec(out);
+ } finally {
+ Arrays.fill(out, (byte) 0);
+ }
+ }
+
+ private static byte[] decodeReplacing(byte[] current, String encoded) {
+ if (current != null) {
+ Arrays.fill(current, (byte) 0);
+ }
+ return Base64.getDecoder().decode(encoded);
+ }
+
+ private String encodedKey() {
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public void destroy() {
+ lifecycleLock.lock();
+ try {
+ if (!destroyed) {
+ Arrays.fill(pkcs8, (byte) 0);
+ destroyed = true;
+ }
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ lifecycleLock.lock();
+ try {
+ return destroyed;
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ private void ensureActive() {
+ if (destroyed) {
+ throw new IllegalStateException("ECDSA private key specification has been destroyed");
+ }
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPublicKeyBuilder.java
index 47f1390..c67dc3f 100644
--- a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPublicKeyBuilder.java
+++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPublicKeyBuilder.java
@@ -39,33 +39,25 @@ import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import zeroecho.core.CryptoAlgorithm;
-import zeroecho.core.CryptoAlgorithms;
-import zeroecho.core.spi.AsymmetricKeyBuilder;
+import zeroecho.core.spi.PublicKeyImporter;
/**
* ECDSA Public Key Builder
*
- * Implementation of {@link AsymmetricKeyBuilder} for
+ * Implementation of {@link zeroecho.core.spi.PublicKeyImporter} for
* {@link EcdsaPublicKeySpec}. This builder is responsible for importing ECDSA
* public keys from X.509 SubjectPublicKeyInfo encodings.
*
- * Supported operations
- *
- * - {@link #importPublic(EcdsaPublicKeySpec)} - construct a {@link PublicKey}
- * instance from an X.509-encoded key.
- * - {@link #generateKeyPair(EcdsaPublicKeySpec)} - unsupported; use
- * {@link EcdsaKeyGenBuilder} instead.
- * - {@link #importPrivate(EcdsaPublicKeySpec)} - unsupported; use
- * {@link EcdsaPrivateKeySpec} with {@link EcdsaPrivateKeyBuilder} instead.
- *
+ * The exact supported operation is
+ * {@link #importPublic(EcdsaPublicKeySpec)}. Generation and private import are
+ * registered through their own operation-specific implementations.
*
* Encoding
The {@link EcdsaPublicKeySpec} stores the public key in
* standard X.509 DER format. This builder delegates to a JCA {@link KeyFactory}
* for the {@code "EC"} algorithm to reconstruct a usable {@link PublicKey}.
*
- * Usage
Typically accessed indirectly through
- * {@link CryptoAlgorithms#publicKey(String, zeroecho.core.spec.AlgorithmKeySpec)}
- * or {@link CryptoAlgorithm#importPublic(zeroecho.core.spec.AlgorithmKeySpec)}.
+ * Usage
Typically accessed through the session key-operation API or
+ * {@link CryptoAlgorithm#publicKeyImporter(Class)}.
*
* {@code
* // Example: Import an ECDSA public key
@@ -76,23 +68,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
*
* @since 1.0
*/
-public final class EcdsaPublicKeyBuilder implements AsymmetricKeyBuilder {
- /**
- * Unsupported operation for this builder.
- *
- *
- * ECDSA key pair generation should be performed using
- * {@link EcdsaKeyGenBuilder}, not from a public key specification.
- *
- *
- * @param spec unused public key specification
- * @return never returns normally
- * @throws UnsupportedOperationException always thrown
- */
- @Override
- public java.security.KeyPair generateKeyPair(EcdsaPublicKeySpec spec) {
- throw new UnsupportedOperationException("Use EcdsaKeyGenBuilder for keypair generation.");
- }
+public final class EcdsaPublicKeyBuilder implements PublicKeyImporter {
/**
* Imports a public key from an X.509 SubjectPublicKeyInfo specification.
@@ -112,21 +88,4 @@ public final class EcdsaPublicKeyBuilder implements AsymmetricKeyBuilder
- * Private key import should be performed using {@link EcdsaPrivateKeySpec} with
- * {@link EcdsaPrivateKeyBuilder}.
- *
- *
- * @param spec unused public key specification
- * @return never returns normally
- * @throws UnsupportedOperationException always thrown
- */
- @Override
- public java.security.PrivateKey importPrivate(EcdsaPublicKeySpec spec) {
- throw new UnsupportedOperationException("Use EcdsaPrivateKeySpec with EcdsaPrivateKeyBuilder.");
- }
}
diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/package-info.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/package-info.java
index 715f765..1a71892 100644
--- a/lib/src/main/java/zeroecho/core/alg/ecdsa/package-info.java
+++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/package-info.java
@@ -36,9 +36,9 @@
*
*
* This package provides the ECDSA algorithm descriptor, curve specifications,
- * key builders for generation and import, and immutable encoded key specs. It
- * wires ECDSA into the core signature SPI through a JCA-backed streaming
- * signature context that enforces fixed signature lengths.
+ * key builders for generation and import, and defensively copying encoded key
+ * specs. It wires ECDSA into the core signature SPI through a JCA-backed
+ * streaming signature context that enforces fixed signature lengths.
*
*
* Scope and responsibilities
@@ -66,8 +66,9 @@
* - EcdsaPublicKeyBuilder and EcdsaPrivateKeyBuilder: import
* keys from X.509 and PKCS#8 encodings via
* {@link java.security.KeyFactory}.
- * - EcdsaPublicKeySpec and EcdsaPrivateKeySpec: immutable
- * wrappers around encoded keys with marshalling support.
+ * - EcdsaPublicKeySpec and EcdsaPrivateKeySpec: wrappers around
+ * encoded keys with marshalling support; the private-key form is
+ * destroyable.
*
*
* Design notes
diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519Algorithm.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519Algorithm.java
index f4116fc..674665b 100644
--- a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519Algorithm.java
+++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519Algorithm.java
@@ -122,9 +122,9 @@ public final class Ed25519Algorithm extends AbstractCryptoAlgorithm {
}, () -> VoidSpec.INSTANCE);
// Key builders
- registerAsymmetricKeyBuilder(Ed25519KeyGenSpec.class, new Ed25519KeyGenBuilder(),
+ registerAsymmetricKeyPairGenerator(Ed25519KeyGenSpec.class, new Ed25519KeyGenBuilder(),
Ed25519KeyGenSpec::defaultSpec);
- registerAsymmetricKeyBuilder(Ed25519PublicKeySpec.class, new Ed25519PublicKeyBuilder(), null);
- registerAsymmetricKeyBuilder(Ed25519PrivateKeySpec.class, new Ed25519PrivateKeyBuilder(), null);
+ registerPublicKeyImporter(Ed25519PublicKeySpec.class, new Ed25519PublicKeyBuilder());
+ registerPrivateKeyImporter(Ed25519PrivateKeySpec.class, new Ed25519PrivateKeyBuilder());
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenBuilder.java
index 7a2e9e7..50a11bc 100644
--- a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenBuilder.java
+++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenBuilder.java
@@ -38,7 +38,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEdDSAKeyGenBuilder;
/**
* Key-pair builder for Ed25519
*
- * Concrete {@link zeroecho.core.spi.AsymmetricKeyBuilder} implementation for
+ * Concrete {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} implementation for
* generating Ed25519 key pairs.
*
*
@@ -50,7 +50,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEdDSAKeyGenBuilder;
*
Usage example
{@code
* // Generate a new Ed25519 key pair with default parameters
* Ed25519KeyGenSpec spec = Ed25519KeyGenSpec.defaultSpec();
- * KeyPair kp = CryptoAlgorithms.keyPair("Ed25519", spec);
+ * KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("Ed25519", spec);
* }
*
* Thread-safety
Instances of this builder are stateless and may be
diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenSpec.java
index bf4d87c..4555f72 100644
--- a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenSpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenSpec.java
@@ -50,7 +50,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* Usage example
{@code
* // Generate a new Ed25519 key pair using the default spec
- * KeyPair kp = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
+ * KeyPair kp = session.keyBuilders().asymmetric()
+ * .generateKeyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
* }
*
* Thread-safety
The default spec instance is immutable and safe to
diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeyBuilder.java
index 63b5e9f..8e324f0 100644
--- a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeyBuilder.java
+++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeyBuilder.java
@@ -38,7 +38,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder;
/**
* Private key builder for Ed25519
*
- * Concrete {@link zeroecho.core.spi.AsymmetricKeyBuilder} for importing and
+ * Concrete {@link zeroecho.core.spi.PrivateKeyImporter} for importing
* wrapping Ed25519 private keys.
*
*
@@ -59,7 +59,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder;
*
Usage example
{@code
* // Import a private key from its encoded PKCS#8 form
* Ed25519PrivateKeySpec spec = new Ed25519PrivateKeySpec(pkcs8Bytes);
- * PrivateKey privateKey = CryptoAlgorithms.privateKey("Ed25519", spec);
+ * PrivateKey privateKey = session.keyBuilders().asymmetric().importPrivate("Ed25519", spec);
* }
*
* Thread-safety
Instances of this builder are stateless and may be
diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeySpec.java
index 15dfdc7..365d23e 100644
--- a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeySpec.java
@@ -33,7 +33,11 @@
******************************************************************************/
package zeroecho.core.alg.ed25519;
+import java.util.Arrays;
import java.util.Base64;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq;
import zeroecho.core.spec.AlgorithmKeySpec;
@@ -65,7 +69,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* Ed25519PrivateKeySpec spec = new Ed25519PrivateKeySpec(pkcs8Bytes);
*
* // Import into a PrivateKey using ZeroEcho
- * PrivateKey priv = CryptoAlgorithms.privateKey("Ed25519", spec);
+ * PrivateKey priv = session.keyBuilders().asymmetric().importPrivate("Ed25519", spec);
*
* // Serialize to PairSeq (e.g., for configuration or transport)
* PairSeq seq = Ed25519PrivateKeySpec.marshal(spec);
@@ -74,16 +78,17 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* Ed25519PrivateKeySpec restored = Ed25519PrivateKeySpec.unmarshal(seq);
* }
*
- * Thread-safety
Instances are immutable. The internal key bytes are
- * defensively copied on construction and retrieval, making this class safe to
- * share across threads.
+ * Thread-safety
Access and destruction are synchronized. The internal
+ * key bytes are defensively copied on construction and retrieval.
*
* @since 1.0
*/
-public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec {
+public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec, Destroyable {
private static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] encodedPkcs8;
+ private final ReentrantLock lifecycleLock = new ReentrantLock();
+ private boolean destroyed;
/**
* Creates a new Ed25519 private key specification from its PKCS#8 encoding.
@@ -104,7 +109,13 @@ public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec {
* @return clone of the PKCS#8 encoding
*/
public byte[] encoded() {
- return encodedPkcs8.clone();
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return encodedPkcs8.clone();
+ } finally {
+ lifecycleLock.unlock();
+ }
}
/**
@@ -119,7 +130,7 @@ public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec {
* @return serialized representation as a {@link PairSeq}
*/
public static PairSeq marshal(Ed25519PrivateKeySpec spec) {
- String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8);
+ String b64 = spec.encodedKey();
return PairSeq.of("type", "Ed25519-PRIV", PKCS8_B64, b64);
}
@@ -143,12 +154,62 @@ public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec {
String k = cur.key();
String v = cur.value();
if (PKCS8_B64.equals(k)) {
- out = Base64.getDecoder().decode(v);
+ out = decodeReplacing(out, v);
}
}
if (out == null) {
throw new IllegalArgumentException("pkcs8.b64 missing for Ed25519 private key");
}
- return new Ed25519PrivateKeySpec(out);
+ try {
+ return new Ed25519PrivateKeySpec(out);
+ } finally {
+ Arrays.fill(out, (byte) 0);
+ }
+ }
+
+ private static byte[] decodeReplacing(byte[] current, String encoded) {
+ if (current != null) {
+ Arrays.fill(current, (byte) 0);
+ }
+ return Base64.getDecoder().decode(encoded);
+ }
+
+ private String encodedKey() {
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return Base64.getEncoder().withoutPadding().encodeToString(encodedPkcs8);
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public void destroy() {
+ lifecycleLock.lock();
+ try {
+ if (!destroyed) {
+ Arrays.fill(encodedPkcs8, (byte) 0);
+ destroyed = true;
+ }
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ lifecycleLock.lock();
+ try {
+ return destroyed;
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ private void ensureActive() {
+ if (destroyed) {
+ throw new IllegalStateException("Ed25519 private key specification has been destroyed");
+ }
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeyBuilder.java
index e335361..886117e 100644
--- a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeyBuilder.java
+++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeyBuilder.java
@@ -38,7 +38,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder;
/**
* Public key builder for Ed25519
*
- * Concrete {@link zeroecho.core.spi.AsymmetricKeyBuilder} for importing and
+ * Concrete {@link zeroecho.core.spi.PublicKeyImporter} for importing
* wrapping Ed25519 public keys.
*
*
@@ -59,7 +59,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder;
*
Usage example
{@code
* // Import a public key from its encoded X.509 form
* Ed25519PublicKeySpec spec = new Ed25519PublicKeySpec(x509Bytes);
- * PublicKey publicKey = CryptoAlgorithms.publicKey("Ed25519", spec);
+ * PublicKey publicKey = session.keyBuilders().asymmetric().importPublic("Ed25519", spec);
* }
*
* Thread-safety
Instances of this builder are stateless and may be
diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeySpec.java
index c32908d..3a34122 100644
--- a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeySpec.java
@@ -65,7 +65,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* Ed25519PublicKeySpec spec = new Ed25519PublicKeySpec(x509Bytes);
*
* // Import into a PublicKey using ZeroEcho
- * PublicKey pub = CryptoAlgorithms.publicKey("Ed25519", spec);
+ * PublicKey pub = session.keyBuilders().asymmetric().importPublic("Ed25519", spec);
*
* // Serialize to PairSeq (e.g., for configuration or transport)
* PairSeq seq = Ed25519PublicKeySpec.marshal(spec);
diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/package-info.java b/lib/src/main/java/zeroecho/core/alg/ed25519/package-info.java
index 22b1961..cb8021c 100644
--- a/lib/src/main/java/zeroecho/core/alg/ed25519/package-info.java
+++ b/lib/src/main/java/zeroecho/core/alg/ed25519/package-info.java
@@ -49,8 +49,8 @@
* enforces the 64-byte tag size.
* - Expose builders for key-pair generation and for importing encoded
* public/private keys.
- * - Define immutable key specifications suitable for safe cloning and simple
- * marshalling.
+ * - Define defensively copying key specifications suitable for safe cloning
+ * and simple marshalling; private-key specifications are destroyable.
*
*
* Components
@@ -63,9 +63,9 @@
* marker spec for producing key pairs.
* - Ed25519PublicKeyBuilder / Ed25519PrivateKeyBuilder:
* importers backed by JCA key factories.
- * - Ed25519PublicKeySpec / Ed25519PrivateKeySpec: immutable
- * wrappers over X.509 and PKCS#8 encodings, with defensive copying and simple
- * base64 marshalling helpers.
+ * - Ed25519PublicKeySpec / Ed25519PrivateKeySpec: wrappers over
+ * X.509 and PKCS#8 encodings, with defensive copying and simple base64
+ * marshalling helpers; the private-key form is destroyable.
*
*
* Design notes
diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448Algorithm.java b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448Algorithm.java
index 0e6259b..e598b02 100644
--- a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448Algorithm.java
+++ b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448Algorithm.java
@@ -82,12 +82,13 @@ import zeroecho.core.spec.VoidSpec;
* {@code
* // Example: generate a key pair and sign data
* CryptoAlgorithm ed448 = new Ed448Algorithm();
- * KeyPair kp = ed448.generateKeyPair(Ed448KeyGenSpec.defaultSpec());
+ * KeyPair kp = ed448.asymmetricKeyPairGenerator(Ed448KeyGenSpec.class)
+ * .generateKeyPair(Ed448KeyGenSpec.defaultSpec());
*
- * SignatureContext signer = ed448.create(KeyUsage.SIGN, kp.getPrivate(), null);
+ * SignatureContext signer = ed448.createContext(KeyUsage.SIGN, kp.getPrivate(), null);
* byte[] sig = signer.sign(data);
*
- * SignatureContext verifier = ed448.create(KeyUsage.VERIFY, kp.getPublic(), null);
+ * SignatureContext verifier = ed448.createContext(KeyUsage.VERIFY, kp.getPublic(), null);
* boolean ok = verifier.verify(data, sig);
* }
*
@@ -128,7 +129,7 @@ public final class Ed448Algorithm extends AbstractCryptoAlgorithm {
* {@code
* // Example: instantiate and obtain a signer
* CryptoAlgorithm ed448 = new Ed448Algorithm();
- * SignatureContext signer = ed448.create(KeyUsage.SIGN, privateKey, null);
+ * SignatureContext signer = ed448.createContext(KeyUsage.SIGN, privateKey, null);
* }
*/
public Ed448Algorithm() {
@@ -155,8 +156,9 @@ public final class Ed448Algorithm extends AbstractCryptoAlgorithm {
}, () -> VoidSpec.INSTANCE);
// Key builders
- registerAsymmetricKeyBuilder(Ed448KeyGenSpec.class, new Ed448KeyGenBuilder(), Ed448KeyGenSpec::defaultSpec);
- registerAsymmetricKeyBuilder(Ed448PublicKeySpec.class, new Ed448PublicKeyBuilder(), null);
- registerAsymmetricKeyBuilder(Ed448PrivateKeySpec.class, new Ed448PrivateKeyBuilder(), null);
+ registerAsymmetricKeyPairGenerator(Ed448KeyGenSpec.class, new Ed448KeyGenBuilder(),
+ Ed448KeyGenSpec::defaultSpec);
+ registerPublicKeyImporter(Ed448PublicKeySpec.class, new Ed448PublicKeyBuilder());
+ registerPrivateKeyImporter(Ed448PrivateKeySpec.class, new Ed448PrivateKeyBuilder());
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeyBuilder.java
index cbd1a9d..883339f 100644
--- a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeyBuilder.java
+++ b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeyBuilder.java
@@ -35,7 +35,6 @@ package zeroecho.core.alg.ed448;
import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder;
import zeroecho.core.spec.AlgorithmKeySpec;
-import zeroecho.core.spi.AsymmetricKeyBuilder;
/**
* Ed448 Private Key Builder
@@ -66,8 +65,8 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* }
*
* Thread-safety
Stateless and safe for concurrent use. Each call to
- * {@link AsymmetricKeyBuilder#importPrivate(AlgorithmKeySpec)
- * importPrivate(Ed448PrivateKeySpec)} creates a new
+ * {@link zeroecho.core.spi.PrivateKeyImporter#importPrivate(AlgorithmKeySpec)}
+ * creates a new
* {@link java.security.KeyFactory}.
*
* @since 1.0
diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeySpec.java
index b216f00..f42c000 100644
--- a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeySpec.java
@@ -33,7 +33,11 @@
******************************************************************************/
package zeroecho.core.alg.ed448;
+import java.util.Arrays;
import java.util.Base64;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq;
import zeroecho.core.spec.AlgorithmKeySpec;
@@ -41,7 +45,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
/**
* Ed448 Private Key Specification
*
- * Immutable specification for an Ed448 private key in PKCS#8 encoding.
+ * Destroyable specification for an Ed448 private key in PKCS#8 encoding.
*
*
* This class acts as a typed carrier for encoded private key material,
@@ -74,15 +78,16 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* Ed448PrivateKeySpec restored = Ed448PrivateKeySpec.unmarshal(p);
* }
*
- * {@code
* CryptoAlgorithm algo = new ElgamalAlgorithm();
- * KeyPair kp = algo.generateKeyPair(ElgamalParamSpec.ffdhe2048());
+ * KeyPair kp = algo.asymmetricKeyPairGenerator(ElgamalParamSpec.class)
+ * .generateKeyPair(ElgamalParamSpec.ffdhe2048());
*
- * EncryptionContext enc = algo.create(KeyUsage.ENCRYPT, kp.getPublic(),
+ * EncryptionContext enc = algo.createContext(KeyUsage.ENCRYPT, kp.getPublic(),
* ElgamalEncSpec.pkcs1());
- * EncryptionContext dec = algo.create(KeyUsage.DECRYPT, kp.getPrivate(),
+ * EncryptionContext dec = algo.createContext(KeyUsage.DECRYPT, kp.getPrivate(),
* ElgamalEncSpec.pkcs1());
* }
*
@@ -148,7 +152,7 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm {
if (false) { // NOPMD
// this key generation is slow
- registerAsymmetricKeyBuilder(ElgamalKeyGenSpec.class, new AsymmetricKeyBuilder<>() {
+ registerAsymmetricKeyPairGenerator(ElgamalKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
@Override
public KeyPair generateKeyPair(ElgamalKeyGenSpec spec) throws GeneralSecurityException {
ensureBC();
@@ -161,20 +165,10 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm {
kpg.initialize(eg, new SecureRandom());
return kpg.generateKeyPair();
}
-
- @Override
- public PublicKey importPublic(ElgamalKeyGenSpec spec) {
- throw new UnsupportedOperationException("Use ElgamalPublicKeySpec to import a public key.");
- }
-
- @Override
- public PrivateKey importPrivate(ElgamalKeyGenSpec spec) {
- throw new UnsupportedOperationException("Use ElgamalPrivateKeySpec to import a private key.");
- }
}, ElgamalKeyGenSpec::elgamal2048);
}
- registerAsymmetricKeyBuilder(ElgamalParamSpec.class, new AsymmetricKeyBuilder<>() {
+ registerAsymmetricKeyPairGenerator(ElgamalParamSpec.class, new AsymmetricKeyPairGenerator<>() {
@Override
public KeyPair generateKeyPair(ElgamalParamSpec spec) throws GeneralSecurityException {
ensureBC();
@@ -183,23 +177,9 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm {
kpg.initialize(eg, new SecureRandom());
return kpg.generateKeyPair();
}
-
- @Override
- public PublicKey importPublic(ElgamalParamSpec spec) {
- throw new UnsupportedOperationException("Use ElgamalPublicKeySpec to import a public key.");
- }
-
- @Override
- public PrivateKey importPrivate(ElgamalParamSpec spec) {
- throw new UnsupportedOperationException("Use ElgamalPrivateKeySpec to import a private key.");
- }
}, ElgamalParamSpec::ffdhe2048);
- registerAsymmetricKeyBuilder(ElgamalPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
- @Override
- public KeyPair generateKeyPair(ElgamalPublicKeySpec spec) {
- throw new UnsupportedOperationException("Generation not supported for encoded spec.");
- }
+ registerPublicKeyImporter(ElgamalPublicKeySpec.class, new PublicKeyImporter<>() {
@Override
public PublicKey importPublic(ElgamalPublicKeySpec spec) throws GeneralSecurityException {
@@ -207,31 +187,22 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm {
KeyFactory kf = KeyFactory.getInstance(EL_GAMAL, providerName());
return kf.generatePublic(new X509EncodedKeySpec(spec.encoded()));
}
+ });
- @Override
- public PrivateKey importPrivate(ElgamalPublicKeySpec spec) {
- throw new UnsupportedOperationException("Use ElgamalPrivateKeySpec for private keys.");
- }
- }, null);
-
- registerAsymmetricKeyBuilder(ElgamalPrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
- @Override
- public KeyPair generateKeyPair(ElgamalPrivateKeySpec spec) {
- throw new UnsupportedOperationException("Generation not supported for encoded spec.");
- }
-
- @Override
- public PublicKey importPublic(ElgamalPrivateKeySpec spec) {
- throw new UnsupportedOperationException("Use ElgamalPublicKeySpec for public keys.");
- }
+ registerPrivateKeyImporter(ElgamalPrivateKeySpec.class, new PrivateKeyImporter<>() {
@Override
public PrivateKey importPrivate(ElgamalPrivateKeySpec spec) throws GeneralSecurityException {
ensureBC();
KeyFactory kf = KeyFactory.getInstance(EL_GAMAL, providerName());
- return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded()));
+ byte[] encoded = spec.encoded();
+ try {
+ return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
+ } finally {
+ Arrays.fill(encoded, (byte) 0);
+ }
}
- }, null);
+ });
}
/**
diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalCipherContext.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalCipherContext.java
index 3a2b7e6..7b5da5b 100644
--- a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalCipherContext.java
+++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalCipherContext.java
@@ -137,7 +137,7 @@ public final class ElgamalCipherContext implements EncryptionContext {
return CipherTransformInputStreamBuilder.builder().withCipher(cipher).withUpstream(upstream)
.withInputBlockSize(g.inputBlockSize()).withOutputBlockSize(g.perBlockOutput())
- .withLeftZeroPadding(g.noPadding).build();
+ .withLeftZeroPadding(g.noPadding).withIndependentBlocks().build();
}
/**
diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalEncSpec.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalEncSpec.java
index 93ce86e..89a69b3 100644
--- a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalEncSpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalEncSpec.java
@@ -71,7 +71,7 @@ import zeroecho.core.spec.ContextSpec;
*
* {@code
* ElgamalEncSpec spec = ElgamalEncSpec.pkcs1();
- * EncryptionContext ctx = algo.create(KeyUsage.ENCRYPT, pubKey, spec);
+ * EncryptionContext ctx = algo.createContext(KeyUsage.ENCRYPT, pubKey, spec);
* }
*
* {@code
* ElgamalKeyGenSpec spec = ElgamalKeyGenSpec.elgamal2048();
- * KeyPair kp = algo.generateKeyPair(spec);
+ * KeyPair kp = algo.asymmetricKeyPairGenerator(ElgamalKeyGenSpec.class).generateKeyPair(spec);
* }
*
* {@code
* ElgamalParamSpec spec = ElgamalParamSpec.ffdhe2048();
- * KeyPair kp = algo.generateKeyPair(spec);
+ * KeyPair kp = algo.asymmetricKeyPairGenerator(ElgamalParamSpec.class).generateKeyPair(spec);
* }
*
* {@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);
* }
*
* - * This class is immutable; the internal byte array is cloned on construction - * and when accessed via {@link #pkcs8()}. + * The internal byte array is cloned on construction and when accessed via + * {@link #pkcs8()}. Access and destruction are synchronized. *
* * @since 1.0 */ -public final class FrodoPrivateKeySpec implements AlgorithmKeySpec { +public final class FrodoPrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] pkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Creates a new specification from a PKCS#8 DER-encoded key. @@ -103,7 +107,13 @@ public final class FrodoPrivateKeySpec implements AlgorithmKeySpec { * @return defensive copy of the PKCS#8-encoded private key */ public byte[] pkcs8() { - return pkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return pkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -114,7 +124,7 @@ public final class FrodoPrivateKeySpec implements AlgorithmKeySpec { * @return a {@code PairSeq} with type and Base64-encoded key */ public static PairSeq marshal(FrodoPrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "FrodoPrivateKeySpec", PKCS8_B64, b64); } @@ -136,7 +146,12 @@ public final class FrodoPrivateKeySpec implements AlgorithmKeySpec { if (b64 == null) { throw new IllegalArgumentException("FrodoPrivateKeySpec: missing pkcs8.b64"); } - return new FrodoPrivateKeySpec(Base64.getDecoder().decode(b64)); + byte[] decoded = Base64.getDecoder().decode(b64); + try { + return new FrodoPrivateKeySpec(decoded); + } finally { + Arrays.fill(decoded, (byte) 0); + } } /** @@ -148,4 +163,43 @@ public final class FrodoPrivateKeySpec implements AlgorithmKeySpec { public String toString() { return "FrodoPrivateKeySpec[len=" + pkcs8.length + "]"; } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(pkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(pkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("Frodo private key specification has been destroyed"); + } + } } diff --git a/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPublicKeySpec.java index afb6d4d..5609ced 100644 --- a/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPublicKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPublicKeySpec.java @@ -36,7 +36,6 @@ package zeroecho.core.alg.frodo; import java.util.Base64; import java.util.Objects; -import zeroecho.core.CryptoAlgorithm; import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq.Cursor; import zeroecho.core.spec.AlgorithmKeySpec; @@ -59,9 +58,8 @@ import zeroecho.core.spec.AlgorithmKeySpec; *{@code
* // Generate a fresh key for HMAC-SHA256
- * SecretKey key = CryptoAlgorithms.generateSecret("HMAC",
+ * SecretKey key = session.keyBuilders().symmetric().generate("HMAC",
* HmacKeyGenSpec.sha256(256));
*
* // Create a MAC context
- * MacContext ctx = CryptoAlgorithms.create("HMAC",
+ * MacContext ctx = session.createContext("HMAC",
* KeyUsage.MAC, key, HmacSpec.sha256());
*
* ctx.update(data);
@@ -133,37 +135,33 @@ public final class HmacAlgorithm extends AbstractCryptoAlgorithm {
try {
return new HmacMacContext(this, k, s.macName());
} catch (GeneralSecurityException e) {
- throw new IOException("Init HMAC failed for " + s.macName(), e);
+ throw new ProviderFailureException("Failed to initialize HMAC " + s.macName(), e);
}
}, HmacSpec::sha256 // default for catalog/tests
);
// Key builders (generation/import) — both respect macName in the spec.
- registerSymmetricKeyBuilder(HmacKeyGenSpec.class, new SymmetricKeyBuilder<>() {
+ registerSymmetricKeyGenerator(HmacKeyGenSpec.class, new SymmetricKeyGenerator<>() {
@Override
public SecretKey generateSecret(HmacKeyGenSpec spec) throws GeneralSecurityException {
KeyGenerator kg = KeyGenerator.getInstance(spec.macName());
kg.init(spec.keySizeBits(), new SecureRandom());
return kg.generateKey();
}
-
- @Override
- public SecretKey importSecret(HmacKeyGenSpec spec) {
- throw new UnsupportedOperationException("Use HmacKeyImportSpec for import");
- }
}, () -> HmacKeyGenSpec.sha256(256) // default keygen spec
);
- registerSymmetricKeyBuilder(HmacKeyImportSpec.class, new SymmetricKeyBuilder<>() {
- @Override
- public SecretKey generateSecret(HmacKeyImportSpec spec) {
- throw new UnsupportedOperationException("Use HmacKeyGenSpec for generation");
- }
+ registerSymmetricKeyImporter(HmacKeyImportSpec.class, new SymmetricKeyImporter<>() {
@Override
public SecretKey importSecret(HmacKeyImportSpec spec) {
- return new SecretKeySpec(spec.key(), spec.macName());
+ byte[] key = spec.key();
+ try {
+ return new SecretKeySpec(key, spec.macName());
+ } finally {
+ Arrays.fill(key, (byte) 0);
+ }
}
- }, null);
+ });
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyGenSpec.java
index 56167e2..cd3df14 100644
--- a/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyGenSpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyGenSpec.java
@@ -55,12 +55,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* Usage
Typical usage is to construct a spec with a given digest
* family and key size, and then pass it to a registered
- * {@code SymmetricKeyBuilder}:
+ * {@link zeroecho.core.spi.SymmetricKeyGenerator}:
*
* {@code
* // Generate a 256-bit key for HMAC-SHA256
* HmacKeyGenSpec spec = HmacKeyGenSpec.sha256(256);
- * SecretKey key = CryptoAlgorithms.generateSecret("HMAC", spec);
+ * SecretKey key = session.keyBuilders().symmetric().generate("HMAC", spec);
* }
*
* Defaults
Convenience static factories are provided for the most
diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyImportSpec.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyImportSpec.java
index fe9087e..98efe41 100644
--- a/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyImportSpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyImportSpec.java
@@ -34,8 +34,12 @@
package zeroecho.core.alg.hmac;
import java.util.Base64;
+import java.util.Arrays;
import java.util.HexFormat;
import java.util.Objects;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.Destroyable;
import zeroecho.core.annotation.Describable;
import zeroecho.core.marshal.PairSeq;
@@ -66,7 +70,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* {@code
* byte[] rawKey = Files.readAllBytes(Paths.get("hmac.key"));
* HmacKeyImportSpec spec = HmacKeyImportSpec.fromRaw("HmacSHA256", rawKey);
- * SecretKey key = CryptoAlgorithms.importSecret("HMAC", spec);
+ * SecretKey key = session.keyBuilders().symmetric().importKey("HMAC", spec);
* }
*
*
@@ -100,9 +104,11 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* @since 1.0
*/
-public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable {
+public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable, Destroyable {
private final String macName;
private final byte[] key;
+ private final ReentrantLock lifecycleLock = new ReentrantLock();
+ private boolean destroyed;
/**
* Constructs a new HMAC key import specification.
@@ -132,7 +138,13 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable {
* @return cloned key bytes
*/
public byte[] key() {
- return key.clone();
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return key.clone();
+ } finally {
+ lifecycleLock.unlock();
+ }
}
/**
@@ -156,7 +168,12 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable {
*/
public static HmacKeyImportSpec fromHex(String macName, String hex) {
Objects.requireNonNull(hex, "hex must not be null");
- return fromRaw(macName, HexFormat.of().parseHex(hex));
+ byte[] decoded = HexFormat.of().parseHex(hex);
+ try {
+ return fromRaw(macName, decoded);
+ } finally {
+ Arrays.fill(decoded, (byte) 0);
+ }
}
/**
@@ -169,7 +186,12 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable {
*/
public static HmacKeyImportSpec fromBase64(String macName, String b64) {
Objects.requireNonNull(b64, "base64 must not be null");
- return fromRaw(macName, Base64.getDecoder().decode(b64));
+ byte[] decoded = Base64.getDecoder().decode(b64);
+ try {
+ return fromRaw(macName, decoded);
+ } finally {
+ Arrays.fill(decoded, (byte) 0);
+ }
}
/**
@@ -193,7 +215,7 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable {
* @return encoded key spec sequence
*/
public static PairSeq marshal(HmacKeyImportSpec spec) {
- String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.key);
+ String b64 = spec.encodedKey();
return PairSeq.of("type", "HMAC-KEY", "mac", spec.macName, "k.b64", b64);
}
@@ -213,24 +235,81 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable {
String mac = null;
byte[] key = null;
- PairSeq.Cursor cur = p.cursor();
- while (cur.next()) {
- String k = cur.key();
- String v = cur.value();
- switch (k) {
- case "mac" -> mac = v;
- case "k.b64" -> key = Base64.getDecoder().decode(v);
- case "k.hex" -> key = HexFormat.of().parseHex(v);
- default -> {
+ try {
+ PairSeq.Cursor cur = p.cursor();
+ while (cur.next()) {
+ String k = cur.key();
+ String v = cur.value();
+ switch (k) {
+ case "mac" -> mac = v;
+ case "k.b64" -> {
+ wipe(key);
+ key = Base64.getDecoder().decode(v);
+ }
+ case "k.hex" -> {
+ wipe(key);
+ key = HexFormat.of().parseHex(v);
+ }
+ default -> {
+ }
}
}
+ if (mac == null) {
+ throw new IllegalArgumentException("mac missing for HMAC key");
+ }
+ if (key == null) {
+ throw new IllegalArgumentException("HMAC key missing (k.b64 or k.hex)");
+ }
+ return new HmacKeyImportSpec(mac, key);
+ } finally {
+ wipe(key);
}
- if (mac == null) {
- throw new IllegalArgumentException("mac missing for HMAC key");
+ }
+
+ private static void wipe(byte[] current) {
+ if (current != null) {
+ Arrays.fill(current, (byte) 0);
}
- if (key == null) {
- throw new IllegalArgumentException("HMAC key missing (k.b64 or k.hex)");
+ }
+
+ private String encodedKey() {
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return Base64.getEncoder().withoutPadding().encodeToString(key);
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void destroy() {
+ lifecycleLock.lock();
+ try {
+ if (!destroyed) {
+ Arrays.fill(key, (byte) 0);
+ destroyed = true;
+ }
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isDestroyed() {
+ lifecycleLock.lock();
+ try {
+ return destroyed;
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ private void ensureActive() {
+ if (destroyed) {
+ throw new IllegalStateException("HMAC key import specification has been destroyed");
}
- return new HmacKeyImportSpec(mac, key);
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/HmacMacContext.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacMacContext.java
index 42a8c3e..9d81f96 100644
--- a/lib/src/main/java/zeroecho/core/alg/hmac/HmacMacContext.java
+++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacMacContext.java
@@ -100,7 +100,7 @@ import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate;
*
* {@code
- * SecretKey key = CryptoAlgorithms.generateSecret("HMAC", HmacKeyGenSpec.sha256(256));
+ * SecretKey key = session.keyBuilders().symmetric().generate("HMAC", HmacKeyGenSpec.sha256(256));
* HmacMacContext ctx = new HmacMacContext(CryptoAlgorithms.require("HMAC"), key, "HmacSHA256");
* try (InputStream in = ctx.wrap(new FileInputStream("data.bin"))) {
* in.transferTo(OutputStream.nullOutputStream()); // body then MAC trailer
@@ -135,7 +135,7 @@ public final class HmacMacContext implements MacContext {
// lifecycle
private boolean wrapped; // = false;
- private Stream activeStream;
+ private HmacStream activeStream;
private boolean autoCloseActiveStream;
/**
@@ -238,7 +238,7 @@ public final class HmacMacContext implements MacContext {
throw new IOException("HMAC init failed for " + macName, e);
}
- Stream s = new Stream(upstream, mac, macName, expectedTag, verifier());
+ HmacStream s = new HmacStream(upstream, mac, macName, expectedTag, verifier());
this.activeStream = s;
return s;
}
diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/HmacSpec.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacSpec.java
index 4cc324a..943f73a 100644
--- a/lib/src/main/java/zeroecho/core/alg/hmac/HmacSpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacSpec.java
@@ -54,10 +54,10 @@ import zeroecho.core.spec.ContextSpec;
*
* Usage
This spec is passed when creating a new HMAC context:
* {@code
- * SecretKey key = CryptoAlgorithms.generateSecret("HMAC",
+ * SecretKey key = session.keyBuilders().symmetric().generate("HMAC",
* HmacKeyGenSpec.sha256(256));
*
- * MacContext ctx = CryptoAlgorithms.create("HMAC",
+ * MacContext ctx = session.createContext("HMAC",
* KeyUsage.MAC, key, HmacSpec.sha256());
*
* ctx.update(data);
diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/Stream.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacStream.java
similarity index 95%
rename from lib/src/main/java/zeroecho/core/alg/hmac/Stream.java
rename to lib/src/main/java/zeroecho/core/alg/hmac/HmacStream.java
index 2867b61..44c4101 100644
--- a/lib/src/main/java/zeroecho/core/alg/hmac/Stream.java
+++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacStream.java
@@ -81,7 +81,7 @@ import zeroecho.core.util.Strings;
* Mac mac = Mac.getInstance("HmacSHA256");
* mac.init(secretKey);
* try (InputStream in = Files.newInputStream(path);
- * InputStream s = new Stream(in, mac, "HmacSHA256", null,
+ * InputStream s = new HmacStream(in, mac, "HmacSHA256", null,
* new ByteVerificationStrategy())) {
* // read from 's' to consume body; trailer is produced automatically
* }
@@ -98,14 +98,14 @@ import zeroecho.core.util.Strings;
* new ByteVerificationStrategy().getThrowOnMismatch();
*
* try (InputStream in = Files.newInputStream(path);
- * InputStream s = new Stream(in, macV, "HmacSHA256", expected, strategy)) {
+ * InputStream s = new HmacStream(in, macV, "HmacSHA256", expected, strategy)) {
* // read from 's'; exception is thrown at EOF if verification fails
* }
* }
*
*/
-final class Stream extends AbstractPassthroughInputStream {
- private static final Logger LOG = Logger.getLogger(Stream.class.getName());
+final class HmacStream extends AbstractPassthroughInputStream {
+ private static final Logger LOG = Logger.getLogger(HmacStream.class.getName());
private final Mac mac;
private final String macName;
@@ -135,7 +135,7 @@ final class Stream extends AbstractPassthroughInputStream {
* @throws NullPointerException if {@code upstream}, {@code mac}, or
* {@code macName} is {@code null}
*/
- /* package */ Stream(final InputStream upstream, final Mac mac, final String macName, final byte[] expectedTag,
+ /* package */ HmacStream(final InputStream upstream, final Mac mac, final String macName, final byte[] expectedTag,
final VerificationBiPredicate verificationStrategy) {
super(upstream, 8192);
this.mac = mac;
diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/package-info.java b/lib/src/main/java/zeroecho/core/alg/hmac/package-info.java
index 74eed74..eb2d680 100644
--- a/lib/src/main/java/zeroecho/core/alg/hmac/package-info.java
+++ b/lib/src/main/java/zeroecho/core/alg/hmac/package-info.java
@@ -48,8 +48,8 @@
* - 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.
- * - Provide immutable specs for selecting the HMAC variant and for supplying
- * keys (generation or import of raw key material).
+ * - Provide immutable specs for selecting the HMAC variant and destroyable
+ * specs for importing raw key material.
* - Encapsulate JCA/JCE interop and provider checks behind small
* factories.
*
@@ -68,7 +68,7 @@
* specific HMAC variant.
* - HmacKeyImportSpec: wrapper for importing existing raw keys, with
* Base64/hex helpers.
- * - Stream: internal passthrough input stream implementing the
+ *
- HmacStream: internal passthrough input stream implementing the
* byte-pumping and trailer/verification logic for the MAC context.
*
*
diff --git a/lib/src/main/java/zeroecho/core/alg/hqc/HqcAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/hqc/HqcAlgorithm.java
index 329f3f7..30632ae 100644
--- a/lib/src/main/java/zeroecho/core/alg/hqc/HqcAlgorithm.java
+++ b/lib/src/main/java/zeroecho/core/alg/hqc/HqcAlgorithm.java
@@ -44,6 +44,7 @@ import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Arrays;
import java.security.spec.X509EncodedKeySpec;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
@@ -56,7 +57,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter;
import zeroecho.core.context.KemContext;
import zeroecho.core.context.MessageAgreementContext;
import zeroecho.core.spec.VoidSpec;
-import zeroecho.core.spi.AsymmetricKeyBuilder;
+import zeroecho.core.spi.AsymmetricKeyPairGenerator;
+import zeroecho.core.spi.PrivateKeyImporter;
+import zeroecho.core.spi.PublicKeyImporter;
/**
* HQC (Hamming Quasi-Cyclic) Algorithm Integration
@@ -124,13 +127,14 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* Usage example
{@code
* // Generate an HQC key pair
* HqcAlgorithm hqc = new HqcAlgorithm();
- * KeyPair kp = hqc.generateKeyPair(HqcKeyGenSpec.hqc256());
+ * KeyPair kp = hqc.asymmetricKeyPairGenerator(HqcKeyGenSpec.class)
+ * .generateKeyPair(HqcKeyGenSpec.hqc256());
*
* // Encapsulation by initiator
- * KemContext encapsCtx = hqc.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
+ * KemContext encapsCtx = hqc.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
*
* // Decapsulation by responder
- * KemContext decapsCtx = hqc.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
+ * KemContext decapsCtx = hqc.createContext(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
* }
*
* @since 1.0
@@ -200,7 +204,7 @@ public final class HqcAlgorithm extends AbstractCryptoAlgorithm {
.build();
}, () -> VoidSpec.INSTANCE);
- registerAsymmetricKeyBuilder(HqcKeyGenSpec.class, new AsymmetricKeyBuilder<>() {
+ registerAsymmetricKeyPairGenerator(HqcKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
@Override
public KeyPair generateKeyPair(HqcKeyGenSpec spec) throws GeneralSecurityException {
ensureProvider();
@@ -213,23 +217,9 @@ public final class HqcAlgorithm extends AbstractCryptoAlgorithm {
kpg.initialize(params, new SecureRandom());
return kpg.generateKeyPair();
}
-
- @Override
- public PublicKey importPublic(HqcKeyGenSpec spec) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public PrivateKey importPrivate(HqcKeyGenSpec spec) {
- throw new UnsupportedOperationException();
- }
}, HqcKeyGenSpec::hqc256);
- registerAsymmetricKeyBuilder(HqcPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
- @Override
- public KeyPair generateKeyPair(HqcPublicKeySpec spec) {
- throw new UnsupportedOperationException();
- }
+ registerPublicKeyImporter(HqcPublicKeySpec.class, new PublicKeyImporter<>() {
@Override
public PublicKey importPublic(HqcPublicKeySpec spec) throws GeneralSecurityException {
@@ -237,31 +227,22 @@ public final class HqcAlgorithm extends AbstractCryptoAlgorithm {
KeyFactory kf = KeyFactory.getInstance("HQC", providerName());
return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
}
+ });
- @Override
- public PrivateKey importPrivate(HqcPublicKeySpec spec) {
- throw new UnsupportedOperationException();
- }
- }, null);
-
- registerAsymmetricKeyBuilder(HqcPrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
- @Override
- public KeyPair generateKeyPair(HqcPrivateKeySpec spec) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public PublicKey importPublic(HqcPrivateKeySpec spec) {
- throw new UnsupportedOperationException();
- }
+ registerPrivateKeyImporter(HqcPrivateKeySpec.class, new PrivateKeyImporter<>() {
@Override
public PrivateKey importPrivate(HqcPrivateKeySpec spec) throws GeneralSecurityException {
ensureProvider();
KeyFactory kf = KeyFactory.getInstance("HQC", providerName());
- return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8()));
+ byte[] encoded = spec.pkcs8();
+ try {
+ return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
+ } finally {
+ Arrays.fill(encoded, (byte) 0);
+ }
}
- }, null);
+ });
}
private static void ensureProvider() throws NoSuchProviderException {
diff --git a/lib/src/main/java/zeroecho/core/alg/hqc/HqcKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/hqc/HqcKeyGenSpec.java
index 51de587..e7ab466 100644
--- a/lib/src/main/java/zeroecho/core/alg/hqc/HqcKeyGenSpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/hqc/HqcKeyGenSpec.java
@@ -65,7 +65,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* // Generate a key pair via HqcAlgorithm
* HqcAlgorithm hqc = new HqcAlgorithm();
- * KeyPair kp = hqc.generateKeyPair(spec);
+ * KeyPair kp = hqc.asymmetricKeyPairGenerator(HqcKeyGenSpec.class).generateKeyPair(spec);
* }
*
* @since 1.0
diff --git a/lib/src/main/java/zeroecho/core/alg/hqc/HqcPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/hqc/HqcPrivateKeySpec.java
index ba4dbfb..95314df 100644
--- a/lib/src/main/java/zeroecho/core/alg/hqc/HqcPrivateKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/hqc/HqcPrivateKeySpec.java
@@ -33,8 +33,12 @@
******************************************************************************/
package zeroecho.core.alg.hqc;
+import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq;
import zeroecho.core.marshal.PairSeq.Cursor;
@@ -48,8 +52,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* * This class is used to transport and import HQC private keys into the - * {@link HqcAlgorithm}. The encoded form is immutable and defensively copied on - * construction and retrieval. + * {@link HqcAlgorithm}. The encoded form is defensively copied on construction + * and retrieval and may be destroyed. *
* *{@code
* CryptoAlgorithm kyber = new KyberAlgorithm();
- * KeyPair kp = kyber.asymmetricKeyBuilder(KyberKeyGenSpec.class)
+ * KeyPair kp = kyber.asymmetricKeyPairGenerator(KyberKeyGenSpec.class)
* .generateKeyPair(KyberKeyGenSpec.kyber768());
* }
*
@@ -63,7 +63,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
*
* @see KyberAlgorithm
- * @see zeroecho.core.CryptoAlgorithm#generateKeyPair(zeroecho.core.spec.AlgorithmKeySpec)
+ * @see zeroecho.sdk.KeyBuilders.Asymmetric#generateKeyPair(String, AlgorithmKeySpec)
*/
public final class KyberKeyGenSpec implements AlgorithmKeySpec, Describable {
/**
diff --git a/lib/src/main/java/zeroecho/core/alg/kyber/KyberPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/kyber/KyberPrivateKeySpec.java
index d2a08c0..5e088be 100644
--- a/lib/src/main/java/zeroecho/core/alg/kyber/KyberPrivateKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/kyber/KyberPrivateKeySpec.java
@@ -33,8 +33,12 @@
******************************************************************************/
package zeroecho.core.alg.kyber;
+import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq;
import zeroecho.core.spec.AlgorithmKeySpec;
@@ -43,7 +47,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* Specification wrapper for a Kyber (ML-KEM) private key encoded in PKCS#8.
*
* - * Instances of this class carry an immutable copy of the PKCS#8-encoded private + * Instances of this class carry an owned copy of the PKCS#8-encoded private * key bytes. They are used with {@link zeroecho.core.CryptoAlgorithm} key * builders to import keys into the provider’s native representation. *
@@ -51,7 +55,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; *- * This class is immutable and thread-safe. + * Access and destruction are synchronized. *
* * @see KyberPublicKeySpec * @see KyberKeyGenSpec */ -public final class KyberPrivateKeySpec implements AlgorithmKeySpec { +public final class KyberPrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] pkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Creates a new spec from PKCS#8-encoded private key bytes. @@ -97,7 +103,13 @@ public final class KyberPrivateKeySpec implements AlgorithmKeySpec { * @return cloned byte array of PKCS#8 DER encoding */ public byte[] pkcs8() { - return pkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return pkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -114,7 +126,7 @@ public final class KyberPrivateKeySpec implements AlgorithmKeySpec { * @return key-value representation suitable for persistence */ public static PairSeq marshal(KyberPrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "KyberPrivateKey", PKCS8_B64, b64); } @@ -139,7 +151,12 @@ public final class KyberPrivateKeySpec implements AlgorithmKeySpec { if (pkcs8b64 == null) { throw new IllegalArgumentException("KyberPrivateKeySpec: missing 'pkcs8.b64'"); } - return new KyberPrivateKeySpec(Base64.getDecoder().decode(pkcs8b64)); + byte[] decoded = Base64.getDecoder().decode(pkcs8b64); + try { + return new KyberPrivateKeySpec(decoded); + } finally { + Arrays.fill(decoded, (byte) 0); + } } /** @@ -155,4 +172,43 @@ public final class KyberPrivateKeySpec implements AlgorithmKeySpec { public String toString() { return "KyberPrivateKeySpec[len=" + pkcs8.length + "]"; } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(pkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(pkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("Kyber private key specification has been destroyed"); + } + } } diff --git a/lib/src/main/java/zeroecho/core/alg/kyber/KyberPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/kyber/KyberPublicKeySpec.java index 40dfc1f..834010f 100644 --- a/lib/src/main/java/zeroecho/core/alg/kyber/KyberPublicKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/kyber/KyberPublicKeySpec.java @@ -62,7 +62,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * // Import a public key into a CryptoAlgorithm * byte[] x509Bytes = ...; // obtained from storage * KyberPublicKeySpec spec = new KyberPublicKeySpec(x509Bytes); - * PublicKey k = kyberAlg.importPublic(spec); + * PublicKey k = kyberAlg.publicKeyImporter(KyberPublicKeySpec.class).importPublic(spec); * * // Serialize for persistence * PairSeq seq = KyberPublicKeySpec.marshal(spec); diff --git a/lib/src/main/java/zeroecho/core/alg/kyber/package-info.java b/lib/src/main/java/zeroecho/core/alg/kyber/package-info.java index 7b9b8f4..eaee39b 100644 --- a/lib/src/main/java/zeroecho/core/alg/kyber/package-info.java +++ b/lib/src/main/java/zeroecho/core/alg/kyber/package-info.java @@ -50,8 +50,9 @@ * message-style agreement adapter where needed. *{@code
* NtruAlgorithm alg = new NtruAlgorithm();
- * KeyPair kpBob = alg.asymmetricKeyBuilder(NtruKeyGenSpec.class)
+ * KeyPair kpBob = alg.asymmetricKeyPairGenerator(NtruKeyGenSpec.class)
* .generateKeyPair(NtruKeyGenSpec.hrss701());
*
* // Alice (initiator) - has Bob's public key
- * MessageAgreementContext alice = alg.create(
+ * MessageAgreementContext alice = alg.createContext(
* KeyUsage.AGREEMENT, kpBob.getPublic(), VoidSpec.INSTANCE);
*
* // Alice produces the peer message she must send to Bob
* byte[] toBob = alice.getPeerMessage();
*
* // Bob (responder) - has his private key
- * MessageAgreementContext bob = alg.create(
+ * MessageAgreementContext bob = alg.createContext(
* KeyUsage.AGREEMENT, kpBob.getPrivate(), VoidSpec.INSTANCE);
*
* // Bob supplies Alice's message so he can complete decapsulation
@@ -200,7 +203,7 @@ public final class NtruAlgorithm extends AbstractCryptoAlgorithm {
}, () -> VoidSpec.INSTANCE);
// Keypair builder via BCPQC
- registerAsymmetricKeyBuilder(NtruKeyGenSpec.class, new AsymmetricKeyBuilder<>() {
+ registerAsymmetricKeyPairGenerator(NtruKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
/**
* Generates a new NTRU key pair using the requested parameter set.
*
@@ -217,45 +220,10 @@ public final class NtruAlgorithm extends AbstractCryptoAlgorithm {
kpg.initialize(params, new SecureRandom());
return kpg.generateKeyPair();
}
-
- /**
- * Importing a public key is not supported by this spec type.
- *
- * @param spec unused
- * @return never returns
- * @throws UnsupportedOperationException always thrown
- */
- @Override
- public PublicKey importPublic(NtruKeyGenSpec spec) {
- throw new UnsupportedOperationException("Import with a dedicated encoded spec, if needed.");
- }
-
- /**
- * Importing a private key is not supported by this spec type.
- *
- * @param spec unused
- * @return never returns
- * @throws UnsupportedOperationException always thrown
- */
- @Override
- public PrivateKey importPrivate(NtruKeyGenSpec spec) {
- throw new UnsupportedOperationException("Import with a dedicated encoded spec, if needed.");
- }
}, NtruKeyGenSpec::hrss701); // sensible default
// Public-key import (X.509)
- registerAsymmetricKeyBuilder(NtruPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
- /**
- * Key generation is not supported for an encoded public-key spec.
- *
- * @param spec unused
- * @return never returns
- * @throws UnsupportedOperationException always thrown
- */
- @Override
- public KeyPair generateKeyPair(NtruPublicKeySpec spec) {
- throw new UnsupportedOperationException("Use NtruKeyGenSpec for generation");
- }
+ registerPublicKeyImporter(NtruPublicKeySpec.class, new PublicKeyImporter<>() {
/**
* Imports a public key from an X.509 SubjectPublicKeyInfo blob.
@@ -270,45 +238,10 @@ public final class NtruAlgorithm extends AbstractCryptoAlgorithm {
KeyFactory kf = KeyFactory.getInstance("NTRU", providerName());
return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
}
-
- /**
- * Private key import is not supported by the public-key spec.
- *
- * @param spec unused
- * @return never returns
- * @throws UnsupportedOperationException always thrown
- */
- @Override
- public PrivateKey importPrivate(NtruPublicKeySpec spec) {
- throw new UnsupportedOperationException("Use NtruPrivateKeySpec for private key import");
- }
- }, null);
+ });
// Private-key import (PKCS#8)
- registerAsymmetricKeyBuilder(NtruPrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
- /**
- * Key generation is not supported for an encoded private-key spec.
- *
- * @param spec unused
- * @return never returns
- * @throws UnsupportedOperationException always thrown
- */
- @Override
- public KeyPair generateKeyPair(NtruPrivateKeySpec spec) {
- throw new UnsupportedOperationException("Use NtruKeyGenSpec for generation");
- }
-
- /**
- * Public key import is not supported by the private-key spec.
- *
- * @param spec unused
- * @return never returns
- * @throws UnsupportedOperationException always thrown
- */
- @Override
- public PublicKey importPublic(NtruPrivateKeySpec spec) {
- throw new UnsupportedOperationException("Use NtruPublicKeySpec for public key import");
- }
+ registerPrivateKeyImporter(NtruPrivateKeySpec.class, new PrivateKeyImporter<>() {
/**
* Imports a private key from a PKCS#8 PrivateKeyInfo blob.
@@ -321,9 +254,14 @@ public final class NtruAlgorithm extends AbstractCryptoAlgorithm {
public PrivateKey importPrivate(NtruPrivateKeySpec spec) throws GeneralSecurityException {
ensureProvider();
KeyFactory kf = KeyFactory.getInstance("NTRU", providerName());
- return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8()));
+ byte[] encoded = spec.pkcs8();
+ try {
+ return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
+ } finally {
+ Arrays.fill(encoded, (byte) 0);
+ }
}
- }, null);
+ });
}
/**
diff --git a/lib/src/main/java/zeroecho/core/alg/ntru/NtruKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/ntru/NtruKeyGenSpec.java
index 8fa82bc..710a23b 100644
--- a/lib/src/main/java/zeroecho/core/alg/ntru/NtruKeyGenSpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/ntru/NtruKeyGenSpec.java
@@ -49,7 +49,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* Usage
{@code
* // Choose a parameter set and generate a key pair
* NtruAlgorithm alg = new NtruAlgorithm();
- * KeyPair kp = alg.asymmetricKeyBuilder(NtruKeyGenSpec.class)
+ * KeyPair kp = alg.asymmetricKeyPairGenerator(NtruKeyGenSpec.class)
* .generateKeyPair(NtruKeyGenSpec.hrss701());
* }
*
diff --git a/lib/src/main/java/zeroecho/core/alg/ntru/NtruPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ntru/NtruPrivateKeySpec.java
index 3d357cf..babd57b 100644
--- a/lib/src/main/java/zeroecho/core/alg/ntru/NtruPrivateKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/ntru/NtruPrivateKeySpec.java
@@ -33,8 +33,12 @@
******************************************************************************/
package zeroecho.core.alg.ntru;
+import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq;
import zeroecho.core.spec.AlgorithmKeySpec;
@@ -44,8 +48,9 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* simple marshal/unmarshal form.
*
*
- * Instances are immutable. The byte array provided to the constructor is
- * defensively copied and {@link #pkcs8()} returns a fresh clone on each call.
+ * The byte array provided to the constructor is defensively copied and
+ * {@link #pkcs8()} returns a fresh clone on each call. Access and destruction
+ * are synchronized.
*
*
* Usage
{@code
@@ -66,10 +71,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* @since 1.0
*/
-public final class NtruPrivateKeySpec implements AlgorithmKeySpec {
+public final class NtruPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
private static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] pkcs8;
+ private final ReentrantLock lifecycleLock = new ReentrantLock();
+ private boolean destroyed;
/**
* Creates a new specification from PKCS#8-encoded bytes.
@@ -96,7 +103,13 @@ public final class NtruPrivateKeySpec implements AlgorithmKeySpec {
* @return a new byte array containing the PKCS#8 DER encoding
*/
public byte[] pkcs8() {
- return pkcs8.clone();
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return pkcs8.clone();
+ } finally {
+ lifecycleLock.unlock();
+ }
}
/**
@@ -118,7 +131,7 @@ public final class NtruPrivateKeySpec implements AlgorithmKeySpec {
* @throws NullPointerException if {@code spec} is null
*/
public static PairSeq marshal(NtruPrivateKeySpec spec) {
- String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
+ String b64 = spec.encodedKey();
return PairSeq.of("type", "NtruPrivateKey", PKCS8_B64, b64);
}
@@ -151,7 +164,12 @@ public final class NtruPrivateKeySpec implements AlgorithmKeySpec {
if (pkcs8b64 == null) {
throw new IllegalArgumentException("NtruPrivateKeySpec: missing 'pkcs8.b64'");
}
- return new NtruPrivateKeySpec(Base64.getDecoder().decode(pkcs8b64));
+ byte[] decoded = Base64.getDecoder().decode(pkcs8b64);
+ try {
+ return new NtruPrivateKeySpec(decoded);
+ } finally {
+ Arrays.fill(decoded, (byte) 0);
+ }
}
/**
@@ -163,4 +181,43 @@ public final class NtruPrivateKeySpec implements AlgorithmKeySpec {
public String toString() {
return "NtruPrivateKeySpec[len=" + pkcs8.length + "]";
}
+
+ private String encodedKey() {
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public void destroy() {
+ lifecycleLock.lock();
+ try {
+ if (!destroyed) {
+ Arrays.fill(pkcs8, (byte) 0);
+ destroyed = true;
+ }
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ lifecycleLock.lock();
+ try {
+ return destroyed;
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ private void ensureActive() {
+ if (destroyed) {
+ throw new IllegalStateException("NTRU private key specification has been destroyed");
+ }
+ }
}
diff --git a/lib/src/main/java/zeroecho/core/alg/ntru/package-info.java b/lib/src/main/java/zeroecho/core/alg/ntru/package-info.java
index 08dc960..949976a 100644
--- a/lib/src/main/java/zeroecho/core/alg/ntru/package-info.java
+++ b/lib/src/main/java/zeroecho/core/alg/ntru/package-info.java
@@ -48,8 +48,9 @@
* DECAPSULATE roles, with an optional message-style agreement adapter.
* {@code
* // Initialize the algorithm and generate a key pair
* NtrulPrimeAlgorithm alg = new NtrulPrimeAlgorithm();
- * KeyPair kp = alg.keys(NtrulPrimeKeyGenSpec.ntrulpr761()).generateKeyPair(NtrulPrimeKeyGenSpec.ntrulpr761());
+ * KeyPair kp = alg.asymmetricKeyPairGenerator(NtrulPrimeKeyGenSpec.class)
+ * .generateKeyPair(NtrulPrimeKeyGenSpec.ntrulpr761());
*
* // Initiator (Alice) encapsulates to Bob's public key
- * MessageAgreementContext alice = alg
- * .context(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class,
- * kp.getPublic(), VoidSpec.INSTANCE);
+ * MessageAgreementContext alice =
+ * alg.createContext(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE);
*
* // Responder (Bob) decapsulates with his private key
- * MessageAgreementContext bob = alg
- * .context(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class,
- * kp.getPrivate(), VoidSpec.INSTANCE);
+ * MessageAgreementContext bob =
+ * alg.createContext(KeyUsage.AGREEMENT, kp.getPrivate(), VoidSpec.INSTANCE);
* }
*
* @see KemContext
@@ -158,7 +160,7 @@ public final class NtrulPrimeAlgorithm extends AbstractCryptoAlgorithm {
.asResponder().build();
}, () -> VoidSpec.INSTANCE);
- registerAsymmetricKeyBuilder(NtrulPrimeKeyGenSpec.class, new AsymmetricKeyBuilder<>() {
+ registerAsymmetricKeyPairGenerator(NtrulPrimeKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
@Override
public KeyPair generateKeyPair(NtrulPrimeKeyGenSpec spec) throws GeneralSecurityException {
ensureProvider();
@@ -174,23 +176,9 @@ public final class NtrulPrimeAlgorithm extends AbstractCryptoAlgorithm {
kpg.initialize(params, new SecureRandom());
return kpg.generateKeyPair();
}
-
- @Override
- public PublicKey importPublic(NtrulPrimeKeyGenSpec spec) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public PrivateKey importPrivate(NtrulPrimeKeyGenSpec spec) {
- throw new UnsupportedOperationException();
- }
}, NtrulPrimeKeyGenSpec::ntrulpr1277);
- registerAsymmetricKeyBuilder(NtrulPrimePublicKeySpec.class, new AsymmetricKeyBuilder<>() {
- @Override
- public KeyPair generateKeyPair(NtrulPrimePublicKeySpec spec) {
- throw new UnsupportedOperationException();
- }
+ registerPublicKeyImporter(NtrulPrimePublicKeySpec.class, new PublicKeyImporter<>() {
@Override
public PublicKey importPublic(NtrulPrimePublicKeySpec spec) throws GeneralSecurityException {
@@ -198,31 +186,22 @@ public final class NtrulPrimeAlgorithm extends AbstractCryptoAlgorithm {
KeyFactory kf = KeyFactory.getInstance("NTRULPRime", providerName());
return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
}
+ });
- @Override
- public PrivateKey importPrivate(NtrulPrimePublicKeySpec spec) {
- throw new UnsupportedOperationException();
- }
- }, null);
-
- registerAsymmetricKeyBuilder(NtrulPrimePrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
- @Override
- public KeyPair generateKeyPair(NtrulPrimePrivateKeySpec spec) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public PublicKey importPublic(NtrulPrimePrivateKeySpec spec) {
- throw new UnsupportedOperationException();
- }
+ registerPrivateKeyImporter(NtrulPrimePrivateKeySpec.class, new PrivateKeyImporter<>() {
@Override
public PrivateKey importPrivate(NtrulPrimePrivateKeySpec spec) throws GeneralSecurityException {
ensureProvider();
KeyFactory kf = KeyFactory.getInstance("NTRULPRime", providerName());
- return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8()));
+ byte[] encoded = spec.pkcs8();
+ try {
+ return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
+ } finally {
+ Arrays.fill(encoded, (byte) 0);
+ }
}
- }, null);
+ });
}
/**
diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeKeyGenSpec.java
index 60241d1..0a379bf 100644
--- a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeKeyGenSpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeKeyGenSpec.java
@@ -57,15 +57,14 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
*
* - * Instances are immutable and typically passed to - * {@link zeroecho.core.CryptoAlgorithm#generateKeyPair} or retrieved from a - * {@code CryptoAlgorithm} builder. For convenience, static factory methods are - * provided for each variant. + * Instances are immutable and passed to + * {@link zeroecho.sdk.KeyBuilders.Asymmetric#generateKeyPair(String, AlgorithmKeySpec)}. + * Static factory methods are provided for each variant. *
* *{@code
- * CryptoAlgorithm alg = CryptoAlgorithms.require("NTRULPRime");
- * KeyPair kp = alg.generateKeyPair(NtrulPrimeKeyGenSpec.ntrulpr761());
+ * KeyPair kp = session.keyBuilders().asymmetric()
+ * .generateKeyPair("NTRULPRime", NtrulPrimeKeyGenSpec.ntrulpr761());
* }
*
* @since 1.0
diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePrivateKeySpec.java
index 752fcb2..ed426a9 100644
--- a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePrivateKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePrivateKeySpec.java
@@ -33,8 +33,12 @@
******************************************************************************/
package zeroecho.core.alg.ntruprime;
+import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq;
import zeroecho.core.marshal.PairSeq.Cursor;
@@ -47,7 +51,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* * {@code NtrulPrimePrivateKeySpec} provides a type-safe holder for * PKCS#8-encoded private key material belonging to the NTRU LPRime KEM. It is - * immutable and defensive copies are made on construction and retrieval. + * destroyable, and defensive copies are made on construction and retrieval. *
* *{@code
* // Initialize the algorithm and generate a key pair
* SntruPrimeAlgorithm alg = new SntruPrimeAlgorithm();
- * KeyPair kp = alg.keys(SntruPrimeKeyGenSpec.sntrup761())
+ * KeyPair kp = alg.asymmetricKeyPairGenerator(SntruPrimeKeyGenSpec.class)
* .generateKeyPair(SntruPrimeKeyGenSpec.sntrup761());
*
* // Initiator (Alice) encapsulates to Bob's public key
@@ -159,7 +162,7 @@ public final class SntruPrimeAlgorithm extends AbstractCryptoAlgorithm {
.asResponder().build();
}, () -> VoidSpec.INSTANCE);
- registerAsymmetricKeyBuilder(SntruPrimeKeyGenSpec.class, new AsymmetricKeyBuilder<>() {
+ registerAsymmetricKeyPairGenerator(SntruPrimeKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
@Override
public KeyPair generateKeyPair(SntruPrimeKeyGenSpec spec) throws GeneralSecurityException {
ensureProvider();
@@ -176,23 +179,9 @@ public final class SntruPrimeAlgorithm extends AbstractCryptoAlgorithm {
kpg.initialize(params, new SecureRandom());
return kpg.generateKeyPair();
}
-
- @Override
- public PublicKey importPublic(SntruPrimeKeyGenSpec spec) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public PrivateKey importPrivate(SntruPrimeKeyGenSpec spec) {
- throw new UnsupportedOperationException();
- }
}, SntruPrimeKeyGenSpec::sntrup1277);
- registerAsymmetricKeyBuilder(SntruPrimePublicKeySpec.class, new AsymmetricKeyBuilder<>() {
- @Override
- public KeyPair generateKeyPair(SntruPrimePublicKeySpec spec) {
- throw new UnsupportedOperationException();
- }
+ registerPublicKeyImporter(SntruPrimePublicKeySpec.class, new PublicKeyImporter<>() {
@Override
public PublicKey importPublic(SntruPrimePublicKeySpec spec) throws GeneralSecurityException {
@@ -200,31 +189,22 @@ public final class SntruPrimeAlgorithm extends AbstractCryptoAlgorithm {
KeyFactory kf = KeyFactory.getInstance("SNTRUPrime", providerName());
return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
}
+ });
- @Override
- public PrivateKey importPrivate(SntruPrimePublicKeySpec spec) {
- throw new UnsupportedOperationException();
- }
- }, null);
-
- registerAsymmetricKeyBuilder(SntruPrimePrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
- @Override
- public KeyPair generateKeyPair(SntruPrimePrivateKeySpec spec) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public PublicKey importPublic(SntruPrimePrivateKeySpec spec) {
- throw new UnsupportedOperationException();
- }
+ registerPrivateKeyImporter(SntruPrimePrivateKeySpec.class, new PrivateKeyImporter<>() {
@Override
public PrivateKey importPrivate(SntruPrimePrivateKeySpec spec) throws GeneralSecurityException {
ensureProvider();
KeyFactory kf = KeyFactory.getInstance("SNTRUPrime", providerName());
- return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8()));
+ byte[] encoded = spec.pkcs8();
+ try {
+ return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
+ } finally {
+ Arrays.fill(encoded, (byte) 0);
+ }
}
- }, null);
+ });
}
/**
diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeKeyGenSpec.java
index ffb3ffe..f060087 100644
--- a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeKeyGenSpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeKeyGenSpec.java
@@ -57,15 +57,14 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
*
*
- * Instances are immutable and typically passed to
- * {@link zeroecho.core.CryptoAlgorithm#generateKeyPair} or retrieved from a
- * {@code CryptoAlgorithm} builder. For convenience, static factory methods are
- * provided for each variant.
+ * Instances are immutable and passed to
+ * {@link zeroecho.sdk.KeyBuilders.Asymmetric#generateKeyPair(String, AlgorithmKeySpec)}.
+ * Static factory methods are provided for each variant.
*
*
* Example
{@code
- * CryptoAlgorithm alg = CryptoAlgorithms.require("SNTRUPrime");
- * KeyPair kp = alg.generateKeyPair(SntruPrimeKeyGenSpec.sntrup761());
+ * KeyPair kp = session.keyBuilders().asymmetric()
+ * .generateKeyPair("SNTRUPrime", SntruPrimeKeyGenSpec.sntrup761());
* }
*
* @since 1.0
diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePrivateKeySpec.java
index 3f33d7f..9b4f0d4 100644
--- a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePrivateKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePrivateKeySpec.java
@@ -33,8 +33,12 @@
******************************************************************************/
package zeroecho.core.alg.ntruprime;
+import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq;
import zeroecho.core.marshal.PairSeq.Cursor;
@@ -47,7 +51,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* {@code SntruPrimePrivateKeySpec} provides a type-safe holder for
* PKCS#8-encoded private key material belonging to the SNTRU Prime KEM. It is
- * immutable and defensive copies are made on construction and retrieval.
+ * destroyable, and defensive copies are made on construction and retrieval.
*
*
* Usage
Instances are typically created after parsing or receiving
@@ -60,7 +64,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* SntruPrimePrivateKeySpec spec = new SntruPrimePrivateKeySpec(pkcs8);
*
* // Import via CryptoAlgorithms
- * PrivateKey priv = CryptoAlgorithms.privateKey("SNTRUPrime", spec);
+ * PrivateKey priv = session.keyBuilders().asymmetric().importPrivate("SNTRUPrime", spec);
* }
*
* @@ -158,7 +161,7 @@ public final class RsaAlgorithm extends AbstractCryptoAlgorithm { }, () -> RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32)); // Key builders - registerAsymmetricKeyBuilder(RsaKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + registerAsymmetricKeyPairGenerator(RsaKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() { @Override public KeyPair generateKeyPair(RsaKeyGenSpec spec) throws GeneralSecurityException { KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); @@ -167,52 +170,29 @@ public final class RsaAlgorithm extends AbstractCryptoAlgorithm { kpg.initialize(params); return kpg.generateKeyPair(); } - - @Override - public PublicKey importPublic(RsaKeyGenSpec spec) { - throw new UnsupportedOperationException("Use RsaPublicKeySpec to import a public key."); - } - - @Override - public PrivateKey importPrivate(RsaKeyGenSpec spec) { - throw new UnsupportedOperationException("Use RsaPrivateKeySpec to import a private key."); - } }, RsaKeyGenSpec::rsa2048); - registerAsymmetricKeyBuilder(RsaPublicKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(RsaPublicKeySpec spec) { - throw new UnsupportedOperationException("Generation not supported for encoded spec."); - } + registerPublicKeyImporter(RsaPublicKeySpec.class, new PublicKeyImporter<>() { @Override public PublicKey importPublic(RsaPublicKeySpec spec) throws GeneralSecurityException { KeyFactory kf = KeyFactory.getInstance("RSA"); return kf.generatePublic(new X509EncodedKeySpec(spec.encoded())); } + }); - @Override - public PrivateKey importPrivate(RsaPublicKeySpec spec) { - throw new UnsupportedOperationException("Use RsaPrivateKeySpec for private keys."); - } - }, null); - - registerAsymmetricKeyBuilder(RsaPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { - @Override - public KeyPair generateKeyPair(RsaPrivateKeySpec spec) { - throw new UnsupportedOperationException("Generation not supported for encoded spec."); - } - - @Override - public PublicKey importPublic(RsaPrivateKeySpec spec) { - throw new UnsupportedOperationException("Use RsaPublicKeySpec for public keys."); - } + registerPrivateKeyImporter(RsaPrivateKeySpec.class, new PrivateKeyImporter<>() { @Override public PrivateKey importPrivate(RsaPrivateKeySpec spec) throws GeneralSecurityException { KeyFactory kf = KeyFactory.getInstance("RSA"); - return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); + byte[] encoded = spec.encoded(); + try { + return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded)); + } finally { + Arrays.fill(encoded, (byte) 0); + } } - }, null); + }); } } diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/RsaCipherContext.java b/lib/src/main/java/zeroecho/core/alg/rsa/RsaCipherContext.java index fdc4798..603dac0 100644 --- a/lib/src/main/java/zeroecho/core/alg/rsa/RsaCipherContext.java +++ b/lib/src/main/java/zeroecho/core/alg/rsa/RsaCipherContext.java @@ -187,9 +187,9 @@ public final class RsaCipherContext implements EncryptionContext { BlockGeometry rsaGeometry = BlockGeometry.forRsa(spec, key, encrypt); return CipherTransformInputStreamBuilder.builder().withUpstream(upstream).withCipher(cipher) - .withInputBlockSize(rsaGeometry.inChunkSize).withOutputBlockSize(rsaGeometry.outChunkSize) - .withBufferedBlocks(100).withFinalizationOutputChunks(rsaGeometry.finalizationOutputChunks) - .withUpdateStreaming(false).build(); + .withInputBlockSize(rsaGeometry.inChunkSize()).withOutputBlockSize(rsaGeometry.outChunkSize()) + .withBufferedBlocks(100).withFinalizationOutputChunks(rsaGeometry.finalizationOutputChunks()) + .withIndependentBlocks().build(); } catch (GeneralSecurityException e) { throw new IOException(spec.description() + " RSA attach/init failed: " + e.getMessage(), e); } diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/RsaKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/rsa/RsaKeyGenSpec.java index 550ef4b..e92b350 100644 --- a/lib/src/main/java/zeroecho/core/alg/rsa/RsaKeyGenSpec.java +++ b/lib/src/main/java/zeroecho/core/alg/rsa/RsaKeyGenSpec.java @@ -59,7 +59,7 @@ import zeroecho.core.spec.AlgorithmKeySpec; * RsaKeyGenSpec spec = RsaKeyGenSpec.rsa2048(); * * // Generate the key pair - * KeyPair kp = rsaAlgorithm.asymmetricKeyBuilder(RsaKeyGenSpec.class) + * KeyPair kp = rsaAlgorithm.asymmetricKeyPairGenerator(RsaKeyGenSpec.class) * .generateKeyPair(spec); * } * diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/RsaPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/rsa/RsaPrivateKeySpec.java index 7fbf36f..0f5cee0 100644 --- a/lib/src/main/java/zeroecho/core/alg/rsa/RsaPrivateKeySpec.java +++ b/lib/src/main/java/zeroecho/core/alg/rsa/RsaPrivateKeySpec.java @@ -33,7 +33,11 @@ ******************************************************************************/ package zeroecho.core.alg.rsa; +import java.util.Arrays; import java.util.Base64; +import java.util.concurrent.locks.ReentrantLock; + +import javax.security.auth.Destroyable; import zeroecho.core.marshal.PairSeq; import zeroecho.core.spec.AlgorithmKeySpec; @@ -64,15 +68,18 @@ import zeroecho.core.spec.AlgorithmKeySpec; * } * *
- * Instances are immutable and defensively copy their input. The encoded key - * material remains sensitive and should be handled with care. + * Instances defensively copy their input and may be destroyed to wipe the owned + * encoding. The encoded key material remains sensitive and should be handled + * with care. *
* * @since 1.0 */ -public final class RsaPrivateKeySpec implements AlgorithmKeySpec { +public final class RsaPrivateKeySpec implements AlgorithmKeySpec, Destroyable { private static final String PKCS8_B64 = "pkcs8.b64"; private final byte[] pkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); + private boolean destroyed; /** * Constructs a new RSA private key spec from a PKCS#8-encoded key. @@ -90,7 +97,13 @@ public final class RsaPrivateKeySpec implements AlgorithmKeySpec { * @return defensive copy of the encoded key */ public byte[] encoded() { - return pkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return pkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -107,7 +120,7 @@ public final class RsaPrivateKeySpec implements AlgorithmKeySpec { * @return pair sequence with type and base64-encoded key material */ public static PairSeq marshal(RsaPrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "RSA-PRIV", PKCS8_B64, b64); } @@ -131,12 +144,62 @@ public final class RsaPrivateKeySpec implements AlgorithmKeySpec { String k = cur.key(); String v = cur.value(); if (PKCS8_B64.equals(k)) { - out = Base64.getDecoder().decode(v); + out = decodeReplacing(out, v); } } if (out == null) { throw new IllegalArgumentException("pkcs8.b64 missing for RSA private key"); } - return new RsaPrivateKeySpec(out); + try { + return new RsaPrivateKeySpec(out); + } finally { + Arrays.fill(out, (byte) 0); + } + } + + private static byte[] decodeReplacing(byte[] current, String encoded) { + if (current != null) { + Arrays.fill(current, (byte) 0); + } + return Base64.getDecoder().decode(encoded); + } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(pkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(pkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("RSA private key specification has been destroyed"); + } } } diff --git a/lib/src/main/java/zeroecho/core/alg/saber/SaberAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/saber/SaberAlgorithm.java index 5912134..70643d1 100644 --- a/lib/src/main/java/zeroecho/core/alg/saber/SaberAlgorithm.java +++ b/lib/src/main/java/zeroecho/core/alg/saber/SaberAlgorithm.java @@ -44,6 +44,7 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Arrays; import java.security.spec.X509EncodedKeySpec; import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; @@ -55,7 +56,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; import zeroecho.core.context.KemContext; import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.spec.VoidSpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.AsymmetricKeyPairGenerator; +import zeroecho.core.spi.PrivateKeyImporter; +import zeroecho.core.spi.PublicKeyImporter; /** * Implements the SABER post-quantum key encapsulation mechanism for the crypto @@ -73,7 +76,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder; * message-oriented interface suitable for one-pass key agreement. * *{@code
* SaberKeyGenSpec spec = SaberKeyGenSpec.saberkem256r3();
- * KeyPair kp = CryptoAlgorithms.keyPair("SABER", spec);
+ * KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("SABER", spec);
* }
*
* @since 1.0
diff --git a/lib/src/main/java/zeroecho/core/alg/saber/SaberPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/saber/SaberPrivateKeySpec.java
index 1bdd27f..6e665af 100644
--- a/lib/src/main/java/zeroecho/core/alg/saber/SaberPrivateKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/saber/SaberPrivateKeySpec.java
@@ -33,8 +33,12 @@
******************************************************************************/
package zeroecho.core.alg.saber;
+import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq;
import zeroecho.core.marshal.PairSeq.Cursor;
@@ -50,23 +54,24 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* representation.
*
*
- * * The internal PKCS#8 encoding is defensively copied on construction and on - * retrieval via {@link #pkcs8()}. Instances are therefore immutable and - * thread-safe. + * retrieval via {@link #pkcs8()}. Access and destruction are synchronized. *
* *{@code
* // Import SABER private key from encoded form
* byte[] pkcs8Bytes = ...;
* SaberPrivateKeySpec spec = new SaberPrivateKeySpec(pkcs8Bytes);
- * PrivateKey key = CryptoAlgorithms.privateKey("SABER", spec);
+ * PrivateKey key = session.keyBuilders().asymmetric().importPrivate("SABER", spec);
* }
*/
-public final class SaberPrivateKeySpec implements AlgorithmKeySpec {
+public final class SaberPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
private static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] pkcs8;
+ private final ReentrantLock lifecycleLock = new ReentrantLock();
+ private boolean destroyed;
/**
* Constructs a private key specification from a PKCS#8-encoded SABER private
@@ -85,7 +90,13 @@ public final class SaberPrivateKeySpec implements AlgorithmKeySpec {
* @return cloned byte array containing the PKCS#8 encoding
*/
public byte[] pkcs8() {
- return pkcs8.clone();
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return pkcs8.clone();
+ } finally {
+ lifecycleLock.unlock();
+ }
}
/**
@@ -103,7 +114,7 @@ public final class SaberPrivateKeySpec implements AlgorithmKeySpec {
* @throws NullPointerException if {@code spec} is {@code null}
*/
public static PairSeq marshal(SaberPrivateKeySpec spec) {
- String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
+ String b64 = spec.encodedKey();
return PairSeq.of("type", "SaberPrivateKeySpec", PKCS8_B64, b64);
}
@@ -129,7 +140,12 @@ public final class SaberPrivateKeySpec implements AlgorithmKeySpec {
if (b64 == null) {
throw new IllegalArgumentException("SaberPrivateKeySpec: missing pkcs8.b64");
}
- return new SaberPrivateKeySpec(Base64.getDecoder().decode(b64));
+ byte[] decoded = Base64.getDecoder().decode(b64);
+ try {
+ return new SaberPrivateKeySpec(decoded);
+ } finally {
+ Arrays.fill(decoded, (byte) 0);
+ }
}
/**
@@ -145,4 +161,43 @@ public final class SaberPrivateKeySpec implements AlgorithmKeySpec {
public String toString() {
return "SaberPrivateKeySpec[len=" + pkcs8.length + "]";
}
+
+ private String encodedKey() {
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public void destroy() {
+ lifecycleLock.lock();
+ try {
+ if (!destroyed) {
+ Arrays.fill(pkcs8, (byte) 0);
+ destroyed = true;
+ }
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ lifecycleLock.lock();
+ try {
+ return destroyed;
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ private void ensureActive() {
+ if (destroyed) {
+ throw new IllegalStateException("Saber private key specification has been destroyed");
+ }
+ }
}
diff --git a/lib/src/main/java/zeroecho/core/alg/saber/SaberPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/saber/SaberPublicKeySpec.java
index 2482162..03f3375 100644
--- a/lib/src/main/java/zeroecho/core/alg/saber/SaberPublicKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/saber/SaberPublicKeySpec.java
@@ -61,7 +61,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* // Import SABER public key from encoded form
* byte[] x509Bytes = ...;
* SaberPublicKeySpec spec = new SaberPublicKeySpec(x509Bytes);
- * PublicKey key = CryptoAlgorithms.publicKey("SABER", spec);
+ * PublicKey key = session.keyBuilders().asymmetric().importPublic("SABER", spec);
* }
*/
public final class SaberPublicKeySpec implements AlgorithmKeySpec {
diff --git a/lib/src/main/java/zeroecho/core/alg/saber/package-info.java b/lib/src/main/java/zeroecho/core/alg/saber/package-info.java
index f72fdc8..81fc370 100644
--- a/lib/src/main/java/zeroecho/core/alg/saber/package-info.java
+++ b/lib/src/main/java/zeroecho/core/alg/saber/package-info.java
@@ -49,8 +49,9 @@
* KEM.
* - * {@code SlhDsaPrivateKeySpec} is an immutable value object that wraps a + * {@code SlhDsaPrivateKeySpec} is a destroyable value object that wraps a * PKCS#8-encoded SLH-DSA private key together with the JCA provider name that * should be used when importing the key. *
@@ -71,15 +75,17 @@ import zeroecho.core.spec.AlgorithmKeySpec; * *- * Instances are immutable and therefore thread-safe. + * Access and destruction are synchronized. *
* * @since 1.0 */ -public final class SlhDsaPrivateKeySpec implements AlgorithmKeySpec { +public final class SlhDsaPrivateKeySpec implements AlgorithmKeySpec, Destroyable { private final byte[] encodedPkcs8; + private final ReentrantLock lifecycleLock = new ReentrantLock(); private final String providerName; + private boolean destroyed; /** * Creates a new specification using the default provider {@code "BC"}. @@ -117,7 +123,13 @@ public final class SlhDsaPrivateKeySpec implements AlgorithmKeySpec { * @return a copy of the encoded private key */ public byte[] encoded() { - return encodedPkcs8.clone(); + lifecycleLock.lock(); + try { + ensureActive(); + return encodedPkcs8.clone(); + } finally { + lifecycleLock.unlock(); + } } /** @@ -142,7 +154,7 @@ public final class SlhDsaPrivateKeySpec implements AlgorithmKeySpec { * @throws NullPointerException if {@code spec} is {@code null} */ public static PairSeq marshal(SlhDsaPrivateKeySpec spec) { - String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8); + String b64 = spec.encodedKey(); return PairSeq.of("type", "SLHDSA-PRIV", "pkcs8.b64", b64, "provider", spec.providerName); } @@ -162,20 +174,74 @@ public final class SlhDsaPrivateKeySpec implements AlgorithmKeySpec { public static SlhDsaPrivateKeySpec unmarshal(PairSeq p) { byte[] out = null; String prov = "BC"; - PairSeq.Cursor c = p.cursor(); - while (c.next()) { - String k = c.key(); - String v = c.value(); - switch (k) { - case "pkcs8.b64" -> out = Base64.getDecoder().decode(v); - case "provider" -> prov = v; - default -> { + try { + PairSeq.Cursor c = p.cursor(); + while (c.next()) { + String k = c.key(); + String v = c.value(); + switch (k) { + case "pkcs8.b64" -> out = decodeReplacing(out, v); + case "provider" -> prov = v; + default -> { + } } } + if (out == null) { + throw new IllegalArgumentException("pkcs8.b64 missing for SLH-DSA private key"); + } + return new SlhDsaPrivateKeySpec(out, prov); + } finally { + wipe(out); } - if (out == null) { - throw new IllegalArgumentException("pkcs8.b64 missing for SLH-DSA private key"); + } + + private static byte[] decodeReplacing(byte[] current, String encoded) { + wipe(current); + return Base64.getDecoder().decode(encoded); + } + + private static void wipe(byte[] current) { + if (current != null) { + Arrays.fill(current, (byte) 0); + } + } + + private String encodedKey() { + lifecycleLock.lock(); + try { + ensureActive(); + return Base64.getEncoder().withoutPadding().encodeToString(encodedPkcs8); + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void destroy() { + lifecycleLock.lock(); + try { + if (!destroyed) { + Arrays.fill(encodedPkcs8, (byte) 0); + destroyed = true; + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public boolean isDestroyed() { + lifecycleLock.lock(); + try { + return destroyed; + } finally { + lifecycleLock.unlock(); + } + } + + private void ensureActive() { + if (destroyed) { + throw new IllegalStateException("SLH-DSA private key specification has been destroyed"); } - return new SlhDsaPrivateKeySpec(out, prov); } } diff --git a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeyBuilder.java index 8d7fb9c..7619686 100644 --- a/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeyBuilder.java +++ b/lib/src/main/java/zeroecho/core/alg/slhdsa/SlhDsaPublicKeyBuilder.java @@ -35,36 +35,24 @@ package zeroecho.core.alg.slhdsa; import java.security.GeneralSecurityException; import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.PrivateKey; import java.security.PublicKey; import java.security.spec.X509EncodedKeySpec; -import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.PublicKeyImporter; /** * Builder for importing SLH-DSA public keys from encoded specifications. * * @since 1.0 */ -public final class SlhDsaPublicKeyBuilder implements AsymmetricKeyBuilder{@code
* CryptoAlgorithm alg = new SphincsPlusAlgorithm();
- * KeyPair kp = alg.asymmetricKeyBuilder(SphincsPlusKeyGenSpec.class)
+ * KeyPair kp = alg.asymmetricKeyPairGenerator(SphincsPlusKeyGenSpec.class)
* .generateKeyPair(SphincsPlusKeyGenSpec.sphincsPlusSha256_128s());
*
* try (SignatureContext signer =
- * alg.create(KeyUsage.SIGN, kp.getPrivate(), null)) {
+ * alg.createContext(KeyUsage.SIGN, kp.getPrivate(), null)) {
* signer.update(message);
* byte[] sig = signer.sign();
* }
*
* try (SignatureContext verifier =
- * alg.create(KeyUsage.VERIFY, kp.getPublic(), null)) {
+ * alg.createContext(KeyUsage.VERIFY, kp.getPublic(), null)) {
* verifier.update(message);
* boolean ok = verifier.verify(sig);
* }
@@ -151,9 +151,9 @@ public final class SphincsPlusAlgorithm extends AbstractCryptoAlgorithm {
}
}, () -> VoidSpec.INSTANCE);
- registerAsymmetricKeyBuilder(SphincsPlusKeyGenSpec.class, new SphincsPlusKeyGenBuilder(),
+ registerAsymmetricKeyPairGenerator(SphincsPlusKeyGenSpec.class, new SphincsPlusKeyGenBuilder(),
SphincsPlusKeyGenSpec::defaultSpec);
- registerAsymmetricKeyBuilder(SphincsPlusPublicKeySpec.class, new SphincsPlusPublicKeyBuilder(), null);
- registerAsymmetricKeyBuilder(SphincsPlusPrivateKeySpec.class, new SphincsPlusPrivateKeyBuilder(), null);
+ registerPublicKeyImporter(SphincsPlusPublicKeySpec.class, new SphincsPlusPublicKeyBuilder());
+ registerPrivateKeyImporter(SphincsPlusPrivateKeySpec.class, new SphincsPlusPrivateKeyBuilder());
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusKeyGenBuilder.java
index 3fba3cc..8078506 100644
--- a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusKeyGenBuilder.java
+++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusKeyGenBuilder.java
@@ -40,7 +40,7 @@ import java.security.KeyPairGenerator;
import java.util.Locale;
import java.util.Objects;
-import zeroecho.core.spi.AsymmetricKeyBuilder;
+import zeroecho.core.spi.AsymmetricKeyPairGenerator;
/**
* Key pair builder for the SPHINCS+ post-quantum signature scheme.
@@ -54,15 +54,10 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* Reflection is used to avoid a hard dependency on all parameter variants.
*
*
- * Supported flows
- * The exact supported operation is + * {@link #generateKeyPair(SphincsPlusKeyGenSpec)}. Public and private import are + * registered separately for {@link SphincsPlusPublicKeySpec} and + * {@link SphincsPlusPrivateKeySpec}.
* *{@code
* SphincsPlusKeyGenSpec spec =
@@ -73,7 +68,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
*
* @since 1.0
*/
-public final class SphincsPlusKeyGenBuilder implements AsymmetricKeyBuilder {
+public final class SphincsPlusKeyGenBuilder implements AsymmetricKeyPairGenerator {
private static final String ALG = "SPHINCSPlus";
@@ -108,40 +103,6 @@ public final class SphincsPlusKeyGenBuilder implements AsymmetricKeyBuilder
- * Public key import is delegated to {@link SphincsPlusPublicKeySpec} and its
- * associated builder.
- *
- *
- * @param spec ignored
- * @return never returns normally
- * @throws UnsupportedOperationException always
- */
- @Override
- public java.security.PublicKey importPublic(SphincsPlusKeyGenSpec spec) {
- throw new UnsupportedOperationException("Use SphincsPlusPublicKeySpec to import a public key.");
- }
-
- /**
- * Not supported for this builder.
- *
- *
- * Private key import is delegated to {@link SphincsPlusPrivateKeySpec} and its
- * associated builder.
- *
- *
- * @param spec ignored
- * @return never returns normally
- * @throws UnsupportedOperationException always
- */
- @Override
- public java.security.PrivateKey importPrivate(SphincsPlusKeyGenSpec spec) {
- throw new UnsupportedOperationException("Use SphincsPlusPrivateKeySpec to import a private key.");
- }
-
/**
* Resolves the Bouncy Castle parameter spec constant that corresponds to the
* given high-level {@link SphincsPlusKeyGenSpec}.
diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeyBuilder.java
index cb3f288..d93cb1d 100644
--- a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeyBuilder.java
+++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeyBuilder.java
@@ -35,12 +35,11 @@ package zeroecho.core.alg.sphincsplus;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
-import java.security.KeyPair;
import java.security.PrivateKey;
-import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Arrays;
-import zeroecho.core.spi.AsymmetricKeyBuilder;
+import zeroecho.core.spi.PrivateKeyImporter;
/**
* Builder for importing SPHINCS+ private keys from encoded specifications.
@@ -52,14 +51,9 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* pairs but focuses solely on importing private key material.
*
*
- * Supported flows
- *
- * - {@link #importPrivate(SphincsPlusPrivateKeySpec)}: imports a SPHINCS+
- * private key from its encoded PKCS#8 representation.
- * - {@link #generateKeyPair(SphincsPlusPrivateKeySpec)}: not supported.
- * - {@link #importPublic(SphincsPlusPrivateKeySpec)}: not supported; use
- * {@link SphincsPlusPublicKeySpec} and its builder instead.
- *
+ * The exact supported operation is
+ * {@link #importPrivate(SphincsPlusPrivateKeySpec)}. Other key operations are
+ * registered through their own exact interfaces.
*
* Example
{@code
* // Assuming bytes contain a PKCS#8-encoded SPHINCS+ private key:
@@ -72,40 +66,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
*
* @since 1.0
*/
-public final class SphincsPlusPrivateKeyBuilder implements AsymmetricKeyBuilder {
- /**
- * Not supported for this builder.
- *
- *
- * Key pair generation requires a parameter set and is handled by
- * {@link SphincsPlusKeyGenBuilder}. This method always throws.
- *
- *
- * @param spec ignored
- * @return never returns normally
- * @throws UnsupportedOperationException always
- */
- @Override
- public KeyPair generateKeyPair(SphincsPlusPrivateKeySpec spec) {
- throw new UnsupportedOperationException("Generation not supported by this spec.");
- }
-
- /**
- * Not supported for this builder.
- *
- *
- * Public key import should be performed via {@link SphincsPlusPublicKeySpec}
- * and its associated builder.
- *
- *
- * @param spec ignored
- * @return never returns normally
- * @throws UnsupportedOperationException always
- */
- @Override
- public PublicKey importPublic(SphincsPlusPrivateKeySpec spec) {
- throw new UnsupportedOperationException("Use SphincsPlusPublicKeySpec for public keys.");
- }
+public final class SphincsPlusPrivateKeyBuilder implements PrivateKeyImporter {
/**
* Imports a SPHINCS+ private key from PKCS#8 encoding.
@@ -128,6 +89,11 @@ public final class SphincsPlusPrivateKeyBuilder implements AsymmetricKeyBuilder<
public PrivateKey importPrivate(SphincsPlusPrivateKeySpec spec) throws GeneralSecurityException {
KeyFactory kf = (spec.providerName() == null) ? KeyFactory.getInstance("SPHINCSPlus")
: KeyFactory.getInstance("SPHINCSPlus", spec.providerName());
- return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded()));
+ byte[] encoded = spec.encoded();
+ try {
+ return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
+ } finally {
+ Arrays.fill(encoded, (byte) 0);
+ }
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeySpec.java
index 86f3e29..d45f4a5 100644
--- a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeySpec.java
@@ -33,7 +33,11 @@
******************************************************************************/
package zeroecho.core.alg.sphincsplus;
+import java.util.Arrays;
import java.util.Base64;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq;
import zeroecho.core.spec.AlgorithmKeySpec;
@@ -44,7 +48,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* {@code SphincsPlusPrivateKeySpec} wraps a PKCS#8-encoded SPHINCS+ private key
* along with the provider name that should be used for import. It is a simple
- * immutable holder designed for use with {@link SphincsPlusPrivateKeyBuilder}.
+ * destroyable holder designed for use with {@link SphincsPlusPrivateKeyBuilder}.
*
*
* Encoding
@@ -57,8 +61,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* provider) if not explicitly supplied.
*
*
- * Thread-safety
Instances are immutable and can be shared safely
- * across threads. Defensive copies are returned for all sensitive material.
+ * Thread-safety
Access and destruction are synchronized. Defensive
+ * copies are returned for all sensitive material.
*
* Example
{@code
* // Wrap a PKCS#8-encoded private key
@@ -77,9 +81,11 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* @since 1.0
*/
-public final class SphincsPlusPrivateKeySpec implements AlgorithmKeySpec {
+public final class SphincsPlusPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
private final byte[] encodedPkcs8;
+ private final ReentrantLock lifecycleLock = new ReentrantLock();
private final String providerName; // e.g. "BCPQC"
+ private boolean destroyed;
/**
* Constructs a new specification with the default provider {@code "BCPQC"}.
@@ -113,7 +119,13 @@ public final class SphincsPlusPrivateKeySpec implements AlgorithmKeySpec {
* @return clone of the encoded key bytes
*/
public byte[] encoded() {
- return encodedPkcs8.clone();
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return encodedPkcs8.clone();
+ } finally {
+ lifecycleLock.unlock();
+ }
}
/**
@@ -137,7 +149,7 @@ public final class SphincsPlusPrivateKeySpec implements AlgorithmKeySpec {
* @return serialized {@link PairSeq} representation
*/
public static PairSeq marshal(SphincsPlusPrivateKeySpec spec) {
- String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8);
+ String b64 = spec.encodedKey();
return PairSeq.of("type", "SPHINCSPLUS-PRIV", "pkcs8.b64", b64, "provider", spec.providerName);
}
@@ -156,20 +168,74 @@ public final class SphincsPlusPrivateKeySpec implements AlgorithmKeySpec {
public static SphincsPlusPrivateKeySpec unmarshal(PairSeq p) {
byte[] out = null;
String prov = "BCPQC";
- PairSeq.Cursor c = p.cursor();
- while (c.next()) {
- String k = c.key();
- String v = c.value();
- switch (k) {
- case "pkcs8.b64" -> out = Base64.getDecoder().decode(v);
- case "provider" -> prov = v;
- default -> {
+ try {
+ PairSeq.Cursor c = p.cursor();
+ while (c.next()) {
+ String k = c.key();
+ String v = c.value();
+ switch (k) {
+ case "pkcs8.b64" -> out = decodeReplacing(out, v);
+ case "provider" -> prov = v;
+ default -> {
+ }
}
}
+ if (out == null) {
+ throw new IllegalArgumentException("pkcs8.b64 missing for SPHINCS+ private key");
+ }
+ return new SphincsPlusPrivateKeySpec(out, prov);
+ } finally {
+ wipe(out);
}
- if (out == null) {
- throw new IllegalArgumentException("pkcs8.b64 missing for SPHINCS+ private key");
+ }
+
+ private static byte[] decodeReplacing(byte[] current, String encoded) {
+ wipe(current);
+ return Base64.getDecoder().decode(encoded);
+ }
+
+ private static void wipe(byte[] current) {
+ if (current != null) {
+ Arrays.fill(current, (byte) 0);
+ }
+ }
+
+ private String encodedKey() {
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return Base64.getEncoder().withoutPadding().encodeToString(encodedPkcs8);
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public void destroy() {
+ lifecycleLock.lock();
+ try {
+ if (!destroyed) {
+ Arrays.fill(encodedPkcs8, (byte) 0);
+ destroyed = true;
+ }
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ lifecycleLock.lock();
+ try {
+ return destroyed;
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ private void ensureActive() {
+ if (destroyed) {
+ throw new IllegalStateException("SPHINCS+ private key specification has been destroyed");
}
- return new SphincsPlusPrivateKeySpec(out, prov);
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPublicKeyBuilder.java
index d1dade2..5eef249 100644
--- a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPublicKeyBuilder.java
+++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPublicKeyBuilder.java
@@ -35,12 +35,10 @@ package zeroecho.core.alg.sphincsplus;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
-import java.security.KeyPair;
-import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
-import zeroecho.core.spi.AsymmetricKeyBuilder;
+import zeroecho.core.spi.PublicKeyImporter;
/**
* Builder for importing SPHINCS+ public keys from encoded specifications.
@@ -52,14 +50,9 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* pairs, but focuses solely on importing public key material.
*
*
- * Supported flows
- *
- * - {@link #importPublic(SphincsPlusPublicKeySpec)}: imports a SPHINCS+
- * public key from its encoded X.509 representation.
- * - {@link #generateKeyPair(SphincsPlusPublicKeySpec)}: not supported.
- * - {@link #importPrivate(SphincsPlusPublicKeySpec)}: not supported; use
- * {@link SphincsPlusPrivateKeySpec} and its builder instead.
- *
+ * The exact supported operation is
+ * {@link #importPublic(SphincsPlusPublicKeySpec)}. Other key operations are
+ * registered through their own exact interfaces.
*
* Example
{@code
* // Assuming bytes contain an X.509-encoded SPHINCS+ public key:
@@ -72,24 +65,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
*
* @since 1.0
*/
-public final class SphincsPlusPublicKeyBuilder implements AsymmetricKeyBuilder {
-
- /**
- * Not supported for this builder.
- *
- *
- * Key pair generation requires algorithm parameters and is handled by
- * {@link SphincsPlusKeyGenBuilder}. This method always throws.
- *
- *
- * @param spec ignored
- * @return never returns normally
- * @throws UnsupportedOperationException always
- */
- @Override
- public KeyPair generateKeyPair(SphincsPlusPublicKeySpec spec) {
- throw new UnsupportedOperationException("Generation not supported by this spec.");
- }
+public final class SphincsPlusPublicKeyBuilder implements PublicKeyImporter {
/**
* Imports a SPHINCS+ public key from X.509 encoding.
@@ -114,21 +90,4 @@ public final class SphincsPlusPublicKeyBuilder implements AsymmetricKeyBuilder
- * Private key import should be performed via {@link SphincsPlusPrivateKeySpec}
- * and its associated builder.
- *
- *
- * @param spec ignored
- * @return never returns normally
- * @throws UnsupportedOperationException always
- */
- @Override
- public PrivateKey importPrivate(SphincsPlusPublicKeySpec spec) {
- throw new UnsupportedOperationException("Use SphincsPlusPrivateKeySpec for private keys.");
- }
}
diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/package-info.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/package-info.java
index cac1140..54c89de 100644
--- a/lib/src/main/java/zeroecho/core/alg/sphincsplus/package-info.java
+++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/package-info.java
@@ -51,8 +51,9 @@
* the key's parameter set.
* - Provide key builders for generating new key pairs and for importing
* encoded public and private keys.
- * - Expose immutable key specification types that defensively copy sensitive
- * material and support compact marshalling.
+ * - Expose key specification types that defensively copy sensitive material
+ * and support compact marshalling; private-key specifications are
+ * destroyable.
*
*
* Components
@@ -67,8 +68,8 @@
* - SphincsPlusPublicKeyBuilder / SphincsPlusPrivateKeyBuilder:
* importers backed by JCA key factories.
* - SphincsPlusPublicKeySpec / SphincsPlusPrivateKeySpec:
- * immutable wrappers over X.509 and PKCS#8 encodings with marshalling
- * helpers.
+ * wrappers over X.509 and PKCS#8 encodings with marshalling helpers; the
+ * private-key form is destroyable.
*
*
* Design notes
diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/XdhAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/xdh/XdhAlgorithm.java
index 20b8c80..7d120f9 100644
--- a/lib/src/main/java/zeroecho/core/alg/xdh/XdhAlgorithm.java
+++ b/lib/src/main/java/zeroecho/core/alg/xdh/XdhAlgorithm.java
@@ -35,11 +35,11 @@ package zeroecho.core.alg.xdh;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
-import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
import zeroecho.core.AlgorithmFamily;
import zeroecho.core.KeyUsage;
@@ -49,7 +49,8 @@ import zeroecho.core.alg.common.agreement.GenericJcaMessageAgreementContext;
import zeroecho.core.alg.common.agreement.KeyPairKey;
import zeroecho.core.context.AgreementContext;
import zeroecho.core.context.MessageAgreementContext;
-import zeroecho.core.spi.AsymmetricKeyBuilder;
+import zeroecho.core.spi.PrivateKeyImporter;
+import zeroecho.core.spi.PublicKeyImporter;
/**
* Algorithm definition for XDH (elliptic curve Diffie-Hellman) key agreement,
@@ -97,16 +98,16 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* CryptoAlgorithm xdh = new XdhAlgorithm();
*
* // Generate key pairs
- * KeyPair a = xdh.asymmetricKeyBuilder(XdhSpec.class).generateKeyPair(XdhSpec.X25519);
- * KeyPair b = xdh.asymmetricKeyBuilder(XdhSpec.class).generateKeyPair(XdhSpec.X25519);
+ * KeyPair a = xdh.asymmetricKeyPairGenerator(XdhSpec.class).generateKeyPair(XdhSpec.X25519);
+ * KeyPair b = xdh.asymmetricKeyPairGenerator(XdhSpec.class).generateKeyPair(XdhSpec.X25519);
*
* // Perform agreement on side A
- * AgreementContext ctxA = xdh.create(KeyUsage.AGREEMENT, a.getPrivate(), XdhSpec.X25519);
+ * AgreementContext ctxA = xdh.createContext(KeyUsage.AGREEMENT, a.getPrivate(), XdhSpec.X25519);
* ctxA.setPeerPublic(b.getPublic());
* byte[] secretA = ctxA.deriveSecret();
*
* // Perform agreement on side B
- * AgreementContext ctxB = xdh.create(KeyUsage.AGREEMENT, b.getPrivate(), XdhSpec.X25519);
+ * AgreementContext ctxB = xdh.createContext(KeyUsage.AGREEMENT, b.getPrivate(), XdhSpec.X25519);
* ctxB.setPeerPublic(a.getPublic());
* byte[] secretB = ctxB.deriveSecret();
*
@@ -153,42 +154,27 @@ public final class XdhAlgorithm extends AbstractCryptoAlgorithm {
s.keyAgreementName(), null, "XDH", null),
() -> XdhSpec.X25519);
- registerAsymmetricKeyBuilder(XdhSpec.class, new XdhKeyGenBuilder(), () -> XdhSpec.X25519);
- registerAsymmetricKeyBuilder(XdhPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
-
- @Override
- public KeyPair generateKeyPair(XdhPublicKeySpec spec) throws GeneralSecurityException {
- throw new UnsupportedOperationException("Use XdhKeyGenBuilder for keypair generation.");
- }
+ registerAsymmetricKeyPairGenerator(XdhSpec.class, new XdhKeyGenBuilder(), () -> XdhSpec.X25519);
+ registerPublicKeyImporter(XdhPublicKeySpec.class, new PublicKeyImporter<>() {
@Override
public PublicKey importPublic(XdhPublicKeySpec spec) throws GeneralSecurityException {
KeyFactory kf = KeyFactory.getInstance("XDH");
return kf.generatePublic(new X509EncodedKeySpec(spec.encoded()));
}
-
- @Override
- public PrivateKey importPrivate(XdhPublicKeySpec spec) throws GeneralSecurityException {
- throw new UnsupportedOperationException("Use XdhPrivateKeySpec for private key import.");
- }
- }, null);
- registerAsymmetricKeyBuilder(XdhPrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
-
- @Override
- public KeyPair generateKeyPair(XdhPrivateKeySpec spec) throws GeneralSecurityException {
- throw new UnsupportedOperationException("Use XdhKeyGenBuilder for keypair generation.");
- }
-
- @Override
- public PublicKey importPublic(XdhPrivateKeySpec spec) throws GeneralSecurityException {
- throw new UnsupportedOperationException("Use XdhPrivateKeySpec for public key import.");
- }
+ });
+ registerPrivateKeyImporter(XdhPrivateKeySpec.class, new PrivateKeyImporter<>() {
@Override
public PrivateKey importPrivate(XdhPrivateKeySpec spec) throws GeneralSecurityException {
KeyFactory kf = KeyFactory.getInstance("XDH");
- return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded()));
+ byte[] encoded = spec.encoded();
+ try {
+ return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
+ } finally {
+ Arrays.fill(encoded, (byte) 0);
+ }
}
- }, null);
+ });
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/XdhKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/xdh/XdhKeyGenBuilder.java
index 2b19e77..227bfb4 100644
--- a/lib/src/main/java/zeroecho/core/alg/xdh/XdhKeyGenBuilder.java
+++ b/lib/src/main/java/zeroecho/core/alg/xdh/XdhKeyGenBuilder.java
@@ -37,7 +37,7 @@ import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
-import zeroecho.core.spi.AsymmetricKeyBuilder;
+import zeroecho.core.spi.AsymmetricKeyPairGenerator;
/**
* KeyPair generator for XDH curves using the JCA KeyPairGenerator SPI.
@@ -50,9 +50,8 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
*
* Design and scope
*
- * - Generation only: This builder supports key generation.
- * Public/private import is intentionally unsupported and will throw
- * {@link UnsupportedOperationException}.
+ * - Generation only: This implementation exposes only the exact
+ * key-pair generation capability; import operations are registered separately.
* - Provider resolution: The default JCA provider selection is used.
* If a specific provider is required, supply or register one that exposes the
* requested XDH algorithm name.
@@ -69,7 +68,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
*
* @since 1.0
*/
-public final class XdhKeyGenBuilder implements AsymmetricKeyBuilder {
+public final class XdhKeyGenBuilder implements AsymmetricKeyPairGenerator {
/**
* Generates a new XDH key pair using the JCA
* {@link java.security.KeyPairGenerator}.
@@ -95,42 +94,4 @@ public final class XdhKeyGenBuilder implements AsymmetricKeyBuilder {
KeyPairGenerator kpg = KeyPairGenerator.getInstance(spec.kpgName());
return kpg.generateKeyPair();
}
-
- /**
- * Not supported: importing XDH public keys is outside the scope of this
- * builder.
- *
- *
- * Use a dedicated import builder or provider-specific utilities if you need to
- * wrap encoded public keys.
- *
- *
- * @param spec the XDH key specification
- * @return never returns normally
- * @throws UnsupportedOperationException always thrown to indicate unsupported
- * operation
- */
- @Override
- public java.security.PublicKey importPublic(XdhSpec spec) {
- throw new UnsupportedOperationException();
- }
-
- /**
- * Not supported: importing XDH private keys is outside the scope of this
- * builder.
- *
- *
- * Use a dedicated import builder or provider-specific utilities if you need to
- * wrap encoded private keys.
- *
- *
- * @param spec the XDH key specification
- * @return never returns normally
- * @throws UnsupportedOperationException always thrown to indicate unsupported
- * operation
- */
- @Override
- public java.security.PrivateKey importPrivate(XdhSpec spec) {
- throw new UnsupportedOperationException();
- }
}
diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/XdhPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/xdh/XdhPrivateKeySpec.java
index a166676..f570f6b 100644
--- a/lib/src/main/java/zeroecho/core/alg/xdh/XdhPrivateKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/xdh/XdhPrivateKeySpec.java
@@ -33,7 +33,11 @@
******************************************************************************/
package zeroecho.core.alg.xdh;
+import java.util.Arrays;
import java.util.Base64;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq;
import zeroecho.core.spec.AlgorithmKeySpec;
@@ -75,9 +79,11 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* @since 1.0
*/
-public class XdhPrivateKeySpec implements AlgorithmKeySpec {
+public class XdhPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
private static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] pkcs8;
+ private final ReentrantLock lifecycleLock = new ReentrantLock();
+ private boolean destroyed;
/**
* Constructs a new specification from the given PKCS#8-encoded private key.
@@ -98,7 +104,13 @@ public class XdhPrivateKeySpec implements AlgorithmKeySpec {
* @return a clone of the internal key encoding
*/
public byte[] encoded() {
- return pkcs8.clone();
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return pkcs8.clone();
+ } finally {
+ lifecycleLock.unlock();
+ }
}
/**
@@ -117,7 +129,7 @@ public class XdhPrivateKeySpec implements AlgorithmKeySpec {
* @throws NullPointerException if {@code spec} is {@code null}
*/
public static PairSeq marshal(XdhPrivateKeySpec spec) {
- String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
+ String b64 = spec.encodedKey();
return PairSeq.of("type", "XDH-PRIV", PKCS8_B64, b64);
}
@@ -141,12 +153,62 @@ public class XdhPrivateKeySpec implements AlgorithmKeySpec {
String k = cur.key();
String v = cur.value();
if (PKCS8_B64.equals(k)) {
- out = Base64.getDecoder().decode(v);
+ out = decodeReplacing(out, v);
}
}
if (out == null) {
throw new IllegalArgumentException("pkcs8.b64 missing for DH private key");
}
- return new XdhPrivateKeySpec(out);
+ try {
+ return new XdhPrivateKeySpec(out);
+ } finally {
+ Arrays.fill(out, (byte) 0);
+ }
+ }
+
+ private static byte[] decodeReplacing(byte[] current, String encoded) {
+ if (current != null) {
+ Arrays.fill(current, (byte) 0);
+ }
+ return Base64.getDecoder().decode(encoded);
+ }
+
+ private String encodedKey() {
+ lifecycleLock.lock();
+ try {
+ ensureActive();
+ return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public void destroy() {
+ lifecycleLock.lock();
+ try {
+ if (!destroyed) {
+ Arrays.fill(pkcs8, (byte) 0);
+ destroyed = true;
+ }
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ lifecycleLock.lock();
+ try {
+ return destroyed;
+ } finally {
+ lifecycleLock.unlock();
+ }
+ }
+
+ private void ensureActive() {
+ if (destroyed) {
+ throw new IllegalStateException("XDH private key specification has been destroyed");
+ }
}
}
diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/XdhSpec.java b/lib/src/main/java/zeroecho/core/alg/xdh/XdhSpec.java
index 2182df1..a0ab79b 100644
--- a/lib/src/main/java/zeroecho/core/alg/xdh/XdhSpec.java
+++ b/lib/src/main/java/zeroecho/core/alg/xdh/XdhSpec.java
@@ -50,10 +50,10 @@ import zeroecho.core.spec.ContextSpec;
*
* Usage
{@code
* // Generate a key pair for X25519
- * KeyPair kp = CryptoAlgorithms.keyPair("XDH", XdhSpec.X25519);
+ * KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("XDH", XdhSpec.X25519);
*
* // Perform key agreement
- * AgreementContext ctx = CryptoAlgorithms.create("XDH", KeyUsage.AGREEMENT,
+ * AgreementContext ctx = session.createContext("XDH", KeyUsage.AGREEMENT,
* kp.getPrivate(), XdhSpec.X25519);
* ctx.setPeerPublic(peerPublicKey);
* byte[] sharedSecret = ctx.deriveSecret();
diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/package-info.java b/lib/src/main/java/zeroecho/core/alg/xdh/package-info.java
index e1e4eb5..801ddb7 100644
--- a/lib/src/main/java/zeroecho/core/alg/xdh/package-info.java
+++ b/lib/src/main/java/zeroecho/core/alg/xdh/package-info.java
@@ -50,8 +50,8 @@
* suitable for KDF input.
* - Provide key builders for generating key pairs and importing encoded
* public/private keys.
- * - Offer immutable key specifications that defensively copy encoded material
- * and support compact marshalling.
+ * - Offer key specifications that defensively copy encoded material and
+ * support compact marshalling; private-key specifications are destroyable.
*
*
* Components
diff --git a/lib/src/main/java/zeroecho/core/audit/AuditListener.java b/lib/src/main/java/zeroecho/core/audit/AuditListener.java
index 4af37bb..1de88a1 100644
--- a/lib/src/main/java/zeroecho/core/audit/AuditListener.java
+++ b/lib/src/main/java/zeroecho/core/audit/AuditListener.java
@@ -38,9 +38,7 @@ import java.security.KeyPair;
import java.util.Map;
import zeroecho.core.KeyUsage;
-import zeroecho.core.context.CryptoContext;
import zeroecho.core.spec.AlgorithmKeySpec;
-import zeroecho.core.spec.ContextSpec;
/**
* Listener for structured audit events emitted by audited crypto contexts.
@@ -72,33 +70,6 @@ import zeroecho.core.spec.ContextSpec;
*
*/
public interface AuditListener {
- /**
- * Emitted right after a context has been created and wrapped.
- *
- *
- * The callback conveys basic provenance such as provider label, the intended
- * key usage role, and optional key and specification objects. Implementations
- * must not attempt to extract secrets from the provided objects.
- *
- *
- * @param the concrete context specification type
- * @param the concrete key type
- * @param id an algorithm or implementation identifier supplied by the
- * creator; never null but may be a generic label such as
- * "unknown"
- * @param provider a provider or vendor label; may be "unknown"
- * @param role the usage role associated with the context, for example
- * ENCRYPTION or VERIFY
- * @param key the key associated with the context if available, or null if
- * not applicable
- * @param spec the context specification if available, or null if not
- * applicable
- */
- default void onContextCreated(String id, String provider, KeyUsage role,
- K key, S spec) {
- // empty
- }
-
/**
* Emitted after key pair generation via SPI.
*
@@ -138,25 +109,6 @@ public interface AuditListener {
// empty
}
- /**
- * Emitted when a context is closed (generic form).
- *
- *
- * This form mirrors legacy summary notifications and may be emitted in addition
- * to the id-based closure callback.
- *
- *
- * @param id an algorithm or implementation identifier supplied at
- * creation time
- * @param provider a provider or vendor label
- * @param role the usage role associated with the context
- * @param key the primary key associated with the context, or null if
- * unavailable
- */
- default void onContextClosed(String id, String provider, KeyUsage role, Key key) {
- // empty
- }
-
/**
* Emitted when a key was destroyed.
*
@@ -410,21 +362,6 @@ public interface AuditListener {
// empty
}
- /**
- * Legacy single-argument creation callback.
- *
- *
- * Emitted when a context is wrapped. Prefer
- * {@link #onContextCreatedMeta(String, String, String, KeyUsage, String, Map)}
- * for structured metadata and correlation.
- *
- *
- * @param ctx the wrapped crypto context; never null
- */
- default void onContextCreated(CryptoContext ctx) {
- // empty
- }
-
/**
* Legacy cumulative byte counter for any role.
*
diff --git a/lib/src/main/java/zeroecho/core/audit/AuditListeners.java b/lib/src/main/java/zeroecho/core/audit/AuditListeners.java
new file mode 100644
index 0000000..7edf79c
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/audit/AuditListeners.java
@@ -0,0 +1,51 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the conditions in the project LICENSE are met.
+ ******************************************************************************/
+package zeroecho.core.audit;
+
+import java.lang.reflect.Proxy;
+import java.util.Objects;
+
+/**
+ * Utilities for enforcing the best-effort audit-listener contract.
+ *
+ * The returned listener suppresses listener failures without logging callback
+ * arguments, because those arguments may refer to sensitive cryptographic
+ * objects. Cryptographic operation outcomes therefore never depend on an audit
+ * sink's availability.
+ *
+ * @since 1.0
+ */
+public final class AuditListeners {
+ private AuditListeners() {
+ // utility class
+ }
+
+ /**
+ * Returns a listener facade that cannot interrupt the caller.
+ *
+ * @param listener listener to protect
+ * @return non-throwing listener facade
+ * @throws NullPointerException if {@code listener} is {@code null}
+ */
+ public static AuditListener bestEffort(AuditListener listener) {
+ AuditListener target = Objects.requireNonNull(listener, "listener");
+ ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
+ ClassLoader loader = contextLoader == null ? ClassLoader.getSystemClassLoader() : contextLoader;
+ return (AuditListener) Proxy.newProxyInstance(loader,
+ new Class>[] { AuditListener.class }, (proxy, method, arguments) -> {
+ if (method.getDeclaringClass() == Object.class) {
+ return method.invoke(target, arguments);
+ }
+ try {
+ return method.invoke(target, arguments);
+ } catch (ReflectiveOperationException ignored) {
+ return null;
+ }
+ });
+ }
+}
diff --git a/lib/src/main/java/zeroecho/core/audit/AuditMode.java b/lib/src/main/java/zeroecho/core/audit/AuditMode.java
new file mode 100644
index 0000000..fc374a7
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/audit/AuditMode.java
@@ -0,0 +1,31 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the conditions in the project LICENSE are met.
+ ******************************************************************************/
+package zeroecho.core.audit;
+
+/**
+ * Defines the session-owned automatic auditing strategy.
+ *
+ * Audit listener failures are best-effort diagnostics and never change the
+ * outcome of a cryptographic operation.
+ *
+ * @since 1.0
+ */
+public enum AuditMode {
+ /**
+ * Emits one context-creation event and returns the original context.
+ */
+ OFF,
+ /**
+ * Emits one context-creation event and returns an operation-auditing wrapper.
+ */
+ WRAP,
+ /**
+ * Emits no automatic audit events and returns the original context.
+ */
+ MANUAL
+}
diff --git a/lib/src/main/java/zeroecho/core/audit/AuditedContexts.java b/lib/src/main/java/zeroecho/core/audit/AuditedContexts.java
index 754563a..14b2ae5 100644
--- a/lib/src/main/java/zeroecho/core/audit/AuditedContexts.java
+++ b/lib/src/main/java/zeroecho/core/audit/AuditedContexts.java
@@ -37,6 +37,9 @@ import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
@@ -44,6 +47,7 @@ import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
+import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
@@ -101,8 +105,8 @@ import zeroecho.core.spec.ContextSpec;
* - Counting is performed by decorating the returned {@code InputStream}s; no
* buffering beyond normal {@code FilterInputStream} forwarding is
* introduced.
- * - Idempotent wrapping: contexts that are already JDK proxies will be
- * returned unchanged.
+ * - Idempotent wrapping: contexts already wrapped by this utility are returned
+ * unchanged. Unrelated JDK proxies are wrapped normally.
*
*
* Usage example
{@code
@@ -130,9 +134,9 @@ public final class AuditedContexts {
* and error events while preserving the original behavior.
*
*
- * If {@code ctx} is {@code null}, this method returns {@code null}. If
- * {@code ctx} is already a JDK dynamic proxy, the instance is returned as-is to
- * keep wrapping idempotent.
+ * If {@code ctx} is {@code null}, this method returns {@code null}. A context
+ * already backed by this utility's auditing handler is returned as-is to keep
+ * wrapping idempotent; unrelated JDK proxies are wrapped normally.
*
*
*
@@ -149,8 +153,9 @@ public final class AuditedContexts {
* with non-reversible key fingerprints and derived sizes.
*
* Creation metadata includes a generated correlation id, algorithm id, provider
- * label, key fingerprint (if extractable), and a best-effort, non-secret spec
- * summary when available.
+ * label, a public-key fingerprint or safe key type label, and a best-effort,
+ * non-secret spec summary when available. Private and secret key encodings are
+ * never requested.
*
*
* This call does not invoke any context operations other than the minimal,
@@ -170,23 +175,29 @@ public final class AuditedContexts {
* @param role the usage role associated with the context (for example,
* ENCRYPTION, DECRYPTION, SIGNING); must not be {@code null}
* @return the auditing proxy for {@code ctx}, the original {@code ctx} if it is
- * already a proxy, or {@code null} if {@code ctx} is {@code null}
+ * already wrapped by this utility, or {@code null} if {@code ctx} is
+ * {@code null}
*/
public static CryptoContext wrap(final CryptoContext ctx, final AuditListener audit, final KeyUsage role) {
if (ctx == null) {
return null;
}
- if (Proxy.isProxyClass(ctx.getClass())) {
- return ctx; // idempotent
+ if (isAuditedProxy(ctx)) {
+ return ctx;
}
// IMPORTANT: do not call any other wrap(...) here — avoid recursion.
return replaceWithProxy(ctx, audit, role);
}
+ private static boolean isAuditedProxy(CryptoContext context) {
+ return Proxy.isProxyClass(context.getClass())
+ && Proxy.getInvocationHandler(context) instanceof AuditingHandler;
+ }
+
@SuppressWarnings("unchecked")
- private static T replaceWithProxy(final T ctx, final AuditListener audit, final KeyUsage role) { // NOPMD
+ private static T replaceWithProxy(final T ctx, final AuditListener audit, final KeyUsage role) {
Objects.requireNonNull(ctx, "ctx must not be null");
- Objects.requireNonNull(audit, "audit must not be null");
+ AuditListener safeAudit = AuditListeners.bestEffort(Objects.requireNonNull(audit, "audit must not be null"));
Objects.requireNonNull(role, "role must not be null");
// Emit creation metadata if we can resolve it
@@ -195,71 +206,49 @@ public final class AuditedContexts {
String provider = null;
String keyFp;
Map specMeta = null;
- Key keyObj = null;
- ContextSpec specObj = null;
+ AccessorSet accessors = new AccessorSet(ctx);
if (ctx instanceof CryptoContext cctx) { // NOPMD
try {
CryptoAlgorithm alg = cctx.algorithm();
if (alg != null) {
try {
- algoId = safeString(invokeNoArg(alg, "id"));
+ algoId = safeString(alg.id());
} catch (Throwable ignore) { // NOPMD
algoId = alg.getClass().getSimpleName();
}
try {
- provider = safeString(invokeNoArg(alg, "providerLabel"));
+ provider = safeString(alg.providerName());
} catch (Throwable ignore) { // NOPMD
- try {
- provider = safeString(invokeNoArg(alg, "provider"));
- } catch (Throwable ignoredToo) { // NOPMD
- provider = alg.getClass().getPackageName();
- }
+ provider = alg.getClass().getPackageName();
}
}
} catch (Throwable ignore) { // NOPMD
// best-effort
}
try {
- keyObj = cctx.key();
- keyFp = fingerprint(keyObj);
+ keyFp = fingerprint(cctx.key());
} catch (Throwable ignore) { // NOPMD
keyFp = "n/a";
}
try {
// optional: contexts may expose a spec() accessor
- Object s = invokeNoArg(cctx, "spec");
- if (s instanceof ContextSpec) {
- specObj = (ContextSpec) s;
- specMeta = SpecIntrospector.summarize(specObj);
+ Object s = accessors.spec();
+ if (s instanceof ContextSpec contextSpec) {
+ specMeta = SpecIntrospector.summarize(contextSpec);
}
} catch (Throwable ignore) { // NOPMD
- specObj = null;
specMeta = null;
}
- // New-style context-created event with metadata
- audit.onContextCreatedMeta(ctxId, algoId == null ? UNKNOWN : algoId, provider == null ? UNKNOWN : provider,
+ safeAudit.onContextCreatedMeta(ctxId, algoId == null ? UNKNOWN : algoId,
+ provider == null ? UNKNOWN : provider,
role, keyFp, specMeta);
-
- // Back-compat event forms
- try {
- // Generic with id/provider/role/key/spec
- audit.onContextCreated(algoId == null ? UNKNOWN : algoId, provider == null ? UNKNOWN : provider, role,
- keyObj, specObj);
- } catch (Throwable ignore) { // NOPMD
- }
- try {
- // Legacy single-arg callback
- audit.onContextCreated(cctx);
- } catch (Throwable ignore) { // NOPMD
- }
}
ClassLoader cl = ctx.getClass().getClassLoader(); // NOPMD
Class>[] ifaces = allInterfaces(ctx.getClass());
- InvocationHandler handler = new AuditingHandler(ctx, audit, role, ctxId, algoId == null ? UNKNOWN : algoId,
- provider == null ? UNKNOWN : provider, keyObj);
+ InvocationHandler handler = new AuditingHandler(ctx, safeAudit, role, ctxId, accessors);
return (T) Proxy.newProxyInstance(cl, ifaces, handler);
}
@@ -281,9 +270,7 @@ public final class AuditedContexts {
private final KeyUsage role;
private final String ctxId;
- private final String algoId;
- private final String provider;
- private final Key keyForClose; // may be null
+ private final AccessorSet accessors;
private long bodyBytes;
private long trailerBytes;
@@ -294,15 +281,13 @@ public final class AuditedContexts {
private String policyLabel = "UNSET";
private String expectedSource = "provided"; // default for setExpectedTag(byte[])
- private AuditingHandler(Object target, AuditListener audit, KeyUsage role, String ctxId, String algoId,
- String provider, Key keyForClose) {
+ private AuditingHandler(Object target, AuditListener audit, KeyUsage role, String ctxId,
+ AccessorSet accessors) {
this.target = target;
this.audit = audit;
this.role = role;
this.ctxId = ctxId;
- this.algoId = algoId;
- this.provider = provider;
- this.keyForClose = keyForClose;
+ this.accessors = accessors;
this.startNanos = System.nanoTime();
}
@@ -312,7 +297,7 @@ public final class AuditedContexts {
// Object methods
if ("toString".equals(name) && (args == null || args.length == 0)) {
- return "AuditedProxy(" + target + ")";
+ return "AuditedCryptoContext[type=" + target.getClass().getName() + ", role=" + role + "]";
}
if ("hashCode".equals(name) && (args == null || args.length == 0)) {
return System.identityHashCode(proxy);
@@ -326,11 +311,12 @@ public final class AuditedContexts {
// Track tagLength() if requested explicitly
if ("tagLength".equals(name) && (args == null || args.length == 0)) {
- Object res = method.invoke(target, args);
- if (res instanceof Integer) {
- this.tagLen = (Integer) res;
+ Integer resolvedTagLength = accessors.tagLength();
+ if (resolvedTagLength != null) {
+ this.tagLen = resolvedTagLength;
+ return resolvedTagLength;
}
- return res;
+ return method.invoke(target, args);
}
// Track verification policy (label only; do not depend on specific enum type)
@@ -344,6 +330,7 @@ public final class AuditedContexts {
if ("setExpectedTag".equals(name) && args != null && args.length == 1 && args[0] instanceof byte[]) {
this.verifyMode = true;
this.expectedSource = "provided";
+ this.tagLen = ((byte[]) args[0]).length;
return method.invoke(target, args);
}
@@ -352,10 +339,7 @@ public final class AuditedContexts {
// Update tagLen lazily if not known
if (this.tagLen == null) {
try {
- Object tl = target.getClass().getMethod("tagLength").invoke(target);
- if (tl instanceof Integer i) { // NOPMD
- this.tagLen = i;
- }
+ this.tagLen = accessors.tagLength();
} catch (Throwable ignore) { // NOPMD
this.tagLen = null;
}
@@ -375,41 +359,23 @@ public final class AuditedContexts {
// setPeerPublic(PublicKey)
if ("setPeerPublic".equals(name) && args != null && args.length == 1
&& args[0] instanceof PublicKey) {
+ Object result = method.invoke(target, args);
try {
- Object res = method.invoke(target, args);
- try {
- String peerFp = fingerprint((Key) args[0]); // short, non-reversible
- audit.onAgreementPeerSet(ctxId, peerFp);
- } catch (Throwable ignore) { // NOPMD
- }
- return res;
- } catch (Throwable t) { // NOPMD
- Throwable cause = unwrapInvocationTarget(t);
- try {
- audit.onFailure(ctxId, "invoke:setPeerPublic", role.name(), cause);
- } catch (Throwable ignore) { // NOPMD
- }
- throw cause;
+ String peerFingerprint = fingerprint((Key) args[0]);
+ audit.onAgreementPeerSet(ctxId, peerFingerprint);
+ } catch (Throwable ignore) { // NOPMD
}
+ return result;
}
// deriveSecret()
if ("deriveSecret".equals(name) && (args == null || args.length == 0)) {
+ byte[] secret = (byte[]) method.invoke(target);
try {
- byte[] secret = (byte[]) method.invoke(target);
- try {
- audit.onAgreementDerived(ctxId, secret == null ? -1 : secret.length);
- } catch (Throwable ignore) { // NOPMD
- }
- return secret;
- } catch (Throwable t) { // NOPMD
- Throwable cause = unwrapInvocationTarget(t);
- try {
- audit.onFailure(ctxId, "invoke:deriveSecret", role.name(), cause);
- } catch (Throwable ignore) { // NOPMD
- }
- throw cause;
+ audit.onAgreementDerived(ctxId, secret == null ? -1 : secret.length);
+ } catch (Throwable ignore) { // NOPMD
}
+ return secret;
}
}
@@ -418,41 +384,23 @@ public final class AuditedContexts {
// setPeerMessage(byte[])
if ("setPeerMessage".equals(name) && args != null && args.length == 1
&& args[0] instanceof byte[]) {
+ Object result = method.invoke(target, args);
try {
- Object r = method.invoke(target, args);
- try {
- int len = ((byte[]) args[0]).length;
- audit.onAgreementPeerMessageSet(ctxId, len);
- } catch (Throwable ignore) { // NOPMD
- }
- return r;
- } catch (Throwable t) { // NOPMD
- Throwable cause = unwrapInvocationTarget(t);
- try {
- audit.onFailure(ctxId, "invoke:setPeerMessage", role.name(), cause);
- } catch (Throwable ignore) { // NOPMD
- }
- throw cause;
+ int length = ((byte[]) args[0]).length;
+ audit.onAgreementPeerMessageSet(ctxId, length);
+ } catch (Throwable ignore) { // NOPMD
}
+ return result;
}
// getPeerMessage()
if ("getPeerMessage".equals(name) && (args == null || args.length == 0)) {
+ byte[] message = (byte[]) method.invoke(target);
try {
- byte[] msg = (byte[]) method.invoke(target);
- try {
- audit.onAgreementPeerMessageGet(ctxId, msg == null ? -1 : msg.length);
- } catch (Throwable ignore) { // NOPMD
- }
- return msg;
- } catch (Throwable t) { // NOPMD
- Throwable cause = unwrapInvocationTarget(t);
- try {
- audit.onFailure(ctxId, "invoke:getPeerMessage", role.name(), cause);
- } catch (Throwable ignore) { // NOPMD
- }
- throw cause;
+ audit.onAgreementPeerMessageGet(ctxId, message == null ? -1 : message.length);
+ } catch (Throwable ignore) { // NOPMD
}
+ return message;
}
}
@@ -487,10 +435,6 @@ public final class AuditedContexts {
audit.onContextClosed(ctxId, bodyBytes, trailerBytes, durationMs);
} catch (Throwable ignore) { // NOPMD
}
- try {
- audit.onContextClosed(algoId, provider, role, keyForClose);
- } catch (Throwable ignore) { // NOPMD
- }
}
}
@@ -564,7 +508,9 @@ public final class AuditedContexts {
final KeyUsage role, final AuditingHandler h) {
return new FilterInputStream(in) {
private long total;
- private boolean eof;
+ private boolean terminal;
+ private boolean failureReported;
+ private boolean verificationReported;
@Override
public int read() throws IOException {
@@ -605,8 +551,10 @@ public final class AuditedContexts {
@Override
public void close() throws IOException {
try { // NOPMD
- transferTo(OutputStream.nullOutputStream());
- onEof();
+ if (!terminal) {
+ transferTo(OutputStream.nullOutputStream());
+ onEof();
+ }
} finally {
super.close();
}
@@ -621,12 +569,17 @@ public final class AuditedContexts {
}
private void onEof() {
- if (eof) {
+ if (terminal) {
return;
}
- eof = true;
+ terminal = true;
- // Reclassify trailer if tagLen known
+ if (h.verifyMode) {
+ reportVerification(true);
+ return;
+ }
+
+ // Produce-mode streams emit the tag as a trailer.
if (h.tagLen != null && h.tagLen > 0 && h.bodyBytes >= h.tagLen) {
h.bodyBytes -= h.tagLen;
h.trailerBytes += h.tagLen;
@@ -635,36 +588,103 @@ public final class AuditedContexts {
} catch (Throwable ignore) { // NOPMD
}
- // Produce tag or verification result at EOF (no exception thrown)
try {
- if (h.verifyMode) {
- audit.onVerifyResult(ctxId, true, h.policyLabel, h.expectedSource, h.tagLen);
- } else {
- audit.onTagProduced(ctxId, h.tagLen, h.policyLabel);
- }
+ audit.onTagProduced(ctxId, h.tagLen, h.policyLabel);
} catch (Throwable ignore) { // NOPMD
}
}
}
private void onFailureMaybeVerify(IOException ioe) {
- try {
- audit.onFailure(ctxId, "read", role.name(), ioe);
- if (h.verifyMode && h.tagLen != null && h.tagLen > 0) {
- // Heuristic: verification failures commonly bubble as IOException from the
- // engine.
- audit.onVerifyResult(ctxId, false, h.policyLabel, h.expectedSource, h.tagLen);
+ terminal = true;
+ if (!failureReported) {
+ failureReported = true;
+ try {
+ audit.onFailure(ctxId, "read", role.name(), ioe);
+ } catch (Throwable ignore) { // NOPMD
}
+ }
+ if (h.verifyMode) {
+ reportVerification(false);
+ }
+ }
+
+ private void reportVerification(boolean success) {
+ if (verificationReported) {
+ return;
+ }
+ verificationReported = true;
+ int reportedTagLength = h.tagLen == null ? -1 : h.tagLen;
+ try {
+ audit.onVerifyResult(ctxId, success, h.policyLabel, h.expectedSource, reportedTagLength);
} catch (Throwable ignore) { // NOPMD
}
}
};
}
- private static Object invokeNoArg(Object target, String method) throws Throwable {
- Method m = target.getClass().getMethod(method);
- m.setAccessible(true); // NOPMD
- return m.invoke(target);
+ /**
+ * Handler-local, pre-bound optional no-argument accessors.
+ */
+ private static final class AccessorSet {
+ private final MethodHandle specAccessor;
+ private final MethodHandle tagLengthAccessor;
+
+ private AccessorSet(Object target) {
+ this.specAccessor = bindNoArg(target, "spec", ContextSpec.class);
+ this.tagLengthAccessor = bindNoArg(target, "tagLength", Integer.class);
+ }
+
+ private Object spec() throws Throwable {
+ if (specAccessor == null) {
+ return null;
+ }
+ return specAccessor.invokeExact();
+ }
+
+ private Integer tagLength() throws Throwable {
+ if (tagLengthAccessor == null) {
+ return null;
+ }
+ Object result = tagLengthAccessor.invokeExact();
+ return result instanceof Integer ? (Integer) result : null;
+ }
+
+ private static MethodHandle bindNoArg(Object target, String name, Class> expectedType) {
+ MethodHandle handle = bindNoArg(target, target.getClass().getMethods(), name, expectedType);
+ if (handle != null) {
+ return handle;
+ }
+ Class>[] interfaces = allInterfaces(target.getClass());
+ for (Class> type : interfaces) {
+ handle = bindNoArg(target, type.getMethods(), name, expectedType);
+ if (handle != null) {
+ return handle;
+ }
+ }
+ return null;
+ }
+
+ private static MethodHandle bindNoArg(Object target, Method[] methods, String name, Class> expectedType) {
+ for (Method method : methods) {
+ if (!name.equals(method.getName()) || method.getParameterCount() != 0
+ || !compatibleReturnType(method.getReturnType(), expectedType)) {
+ continue;
+ }
+ try {
+ MethodHandle handle = MethodHandles.publicLookup().unreflect(method).bindTo(target);
+ return handle.asType(MethodType.methodType(Object.class));
+ } catch (IllegalAccessException exception) { // NOPMD - try another public declaration
+ // Try another public declaration of the same accessor.
+ }
+ }
+ return null;
+ }
+
+ private static boolean compatibleReturnType(Class> actualType, Class> expectedType) {
+ return expectedType == Integer.class && actualType == Integer.TYPE
+ || expectedType.isAssignableFrom(actualType);
+ }
}
private static Throwable unwrapInvocationTarget(Throwable t) {
@@ -682,22 +702,41 @@ public final class AuditedContexts {
if (key == null) {
return "n/a";
}
+ if (!(key instanceof PublicKey)) {
+ return safeKeyMetadata(key);
+ }
+
+ byte[] workingEncoding = null;
+ byte[] digest = null;
try {
- byte[] enc = key.getEncoded();
- if (enc == null) {
+ workingEncoding = key.getEncoded();
+ if (workingEncoding == null) {
// Non-extractable key: fall back to type information
- return key.getAlgorithm() + ":" + key.getClass().getSimpleName();
+ return safeKeyMetadata(key);
}
- MessageDigest md = MessageDigest.getInstance("SHA-256");
- byte[] d = md.digest(enc);
+ MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+ digest = messageDigest.digest(workingEncoding);
// hex-short: first 8 bytes
- StringBuilder sb = new StringBuilder(2 * 8);
- for (int i = 0; i < Math.min(8, d.length); i++) {
- sb.append(String.format("%02x", d[i]));
+ StringBuilder fingerprint = new StringBuilder(16);
+ for (int index = 0; index < Math.min(8, digest.length); index++) {
+ int value = digest[index] & 0xff;
+ fingerprint.append(Character.forDigit(value >>> 4, 16))
+ .append(Character.forDigit(value & 0x0f, 16));
}
- return key.getAlgorithm() + ":" + sb.toString();
- } catch (NoSuchAlgorithmException e) {
+ return key.getAlgorithm() + ":" + fingerprint;
+ } catch (NoSuchAlgorithmException exception) {
return key.getAlgorithm() + ":fp-error";
+ } finally {
+ if (workingEncoding != null) {
+ Arrays.fill(workingEncoding, (byte) 0);
+ }
+ if (digest != null) {
+ Arrays.fill(digest, (byte) 0);
+ }
}
}
+
+ private static String safeKeyMetadata(Key key) {
+ return key.getAlgorithm() + ":" + key.getClass().getSimpleName();
+ }
}
diff --git a/lib/src/main/java/zeroecho/core/audit/JulAuditListenerStd.java b/lib/src/main/java/zeroecho/core/audit/JulAuditListenerStd.java
index 34e0b34..b586911 100644
--- a/lib/src/main/java/zeroecho/core/audit/JulAuditListenerStd.java
+++ b/lib/src/main/java/zeroecho/core/audit/JulAuditListenerStd.java
@@ -37,24 +37,25 @@ import java.security.Key;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.core.KeyUsage;
-import zeroecho.core.context.CryptoContext;
import zeroecho.core.spec.AlgorithmKeySpec;
-import zeroecho.core.spec.ContextSpec;
/**
* AuditListener implementation that emits structured Java Util Logging records.
*
*
* The listener produces parameterized JUL messages in a stable key=value format
- * suitable for ingestion by log processors. It never logs secret material: keys
- * are represented only by short, non-reversible fingerprints and specification
- * objects are summarized by simple type names.
+ * suitable for ingestion by log processors. It never logs secret material:
+ * public keys may be represented by short, non-reversible fingerprints, while
+ * private and secret keys are represented only by algorithm and implementation
+ * type. Specification objects are summarized by simple type names.
*
*
* Configuration
@@ -75,8 +76,8 @@ import zeroecho.core.spec.ContextSpec;
* // Progress tick:
* PROGRESS ctxId=abc-123 body=4096 trailer=16
*
- * // Failure with stack trace when enabled:
- * FAILURE ctxId=abc-123 stage=read op=wrap error=IOException message=stream closed
+ * // Failure summary:
+ * FAILURE ctxId=abc-123 stage=read op=wrap error=IOException
* }
*
*
@@ -134,7 +135,7 @@ public final class JulAuditListenerStd implements AuditListener {
*
- {@code infoLevel}: {@code Level.INFO}
* - {@code warnLevel}: {@code Level.WARNING}
* - {@code progressLevel}: {@code Level.FINE}
- * - {@code includeStackTraces}: {@code true}
+ * - {@code includeStackTraces}: {@code false}
*
*
* Example
{@code
@@ -152,7 +153,7 @@ public final class JulAuditListenerStd implements AuditListener {
private Level infoLevel = Level.INFO;
private Level warnLevel = Level.WARNING;
private Level progressLevel = Level.FINE;
- private boolean includeStackTraces = true;
+ private boolean includeStackTraces;
/**
* Sets the JUL logger that will receive audit messages.
@@ -202,6 +203,12 @@ public final class JulAuditListenerStd implements AuditListener {
* Controls whether {@link #onFailure(String, String, String, Throwable)}
* appends a stack trace in addition to the structured summary.
*
+ *
+ * Stack traces may contain provider exception messages or application
+ * values. Enabling them is an explicit diagnostic opt-in and requires a
+ * suitably protected log destination.
+ *
+ *
* @param include true to include stack traces, false to omit them
* @return this builder for chaining
*/
@@ -229,44 +236,6 @@ public final class JulAuditListenerStd implements AuditListener {
return new Builder();
}
- /**
- * Logs a structured context creation event.
- *
- * @param context specification type
- * @param key type
- * @param id algorithm or implementation identifier
- * @param provider provider or vendor label
- * @param role key usage role for the context
- * @param key associated key, if any
- * @param spec context specification, if any
- */
- @Override
- public void onContextCreated(String id, String provider, KeyUsage role,
- K key, S spec) {
- if (!log.isLoggable(infoLevel)) {
- return;
- }
- log.log(infoLevel, "CTX_CREATED algo={0} provider={1} role={2} keyFp={3} spec={4}",
- new Object[] { id, provider, role, fingerprint(key), specName(spec) });
- }
-
- /**
- * Logs a structured context closure event in generic form.
- *
- * @param id algorithm or implementation identifier
- * @param provider provider or vendor label
- * @param role key usage role for the context
- * @param key associated key, if any
- */
- @Override
- public void onContextClosed(String id, String provider, KeyUsage role, Key key) {
- if (!log.isLoggable(infoLevel)) {
- return;
- }
- log.log(infoLevel, "CTX_CLOSED algo={0} provider={1} role={2} keyFp={3}",
- new Object[] { id, provider, role, fingerprint(key) });
- }
-
/**
* Logs key pair generation with non-secret metadata.
*
@@ -298,7 +267,7 @@ public final class JulAuditListenerStd implements AuditListener {
if (!log.isLoggable(infoLevel)) {
return;
}
- log.log(infoLevel, "KEY_BUILT algo={0} provider={1} spec={2} keyFp={3}",
+ log.log(infoLevel, "KEY_BUILT algo={0} provider={1} spec={2} key={3}",
new Object[] { id, provider, specType(spec), fingerprint(key) });
}
@@ -314,7 +283,7 @@ public final class JulAuditListenerStd implements AuditListener {
if (!log.isLoggable(infoLevel)) {
return;
}
- log.log(infoLevel, "KEY_DESTROYED algo={0} provider={1} keyFp={2}",
+ log.log(infoLevel, "KEY_DESTROYED algo={0} provider={1} key={2}",
new Object[] { id, provider, fingerprint(key) });
}
@@ -469,9 +438,8 @@ public final class JulAuditListenerStd implements AuditListener {
if (!log.isLoggable(warnLevel)) {
return;
}
- String msg = "FAILURE ctxId={0} stage={1} op={2} error={3} message={4}";
- Object[] params = { ctxId, stage, op, (cause == null ? "unknown" : cause.getClass().getSimpleName()),
- (cause == null ? "" : safeMessage(cause.getMessage())) };
+ String msg = "FAILURE ctxId={0} stage={1} op={2} error={3}";
+ Object[] params = { ctxId, stage, op, (cause == null ? "unknown" : cause.getClass().getSimpleName()) };
if (includeStackTraces && cause != null) {
log.log(warnLevel, msg, params);
log.log(warnLevel, "STACKTRACE", cause);
@@ -522,19 +490,6 @@ public final class JulAuditListenerStd implements AuditListener {
}
}
- /**
- * Logs the legacy single-argument creation callback with the context type.
- *
- * @param ctx the wrapped context
- */
- @Override
- public void onContextCreated(CryptoContext ctx) {
- if (log.isLoggable(progressLevel)) {
- log.log(progressLevel, "CTX_CREATED_LEGACY type={0}",
- new Object[] { (ctx == null ? "null" : ctx.getClass().getSimpleName()) });
- }
- }
-
/**
* Logs the legacy cumulative byte counter.
*
@@ -598,16 +553,6 @@ public final class JulAuditListenerStd implements AuditListener {
}
}
- /**
- * Returns the simple name of the provided specification or "null".
- *
- * @param spec a context specification, possibly null
- * @return simple class name or "null"
- */
- private static String specName(ContextSpec spec) {
- return spec == null ? "null" : spec.getClass().getSimpleName();
- }
-
/**
* Returns the simple name of the provided key specification or "null".
*
@@ -629,8 +574,9 @@ public final class JulAuditListenerStd implements AuditListener {
}
/**
- * Computes a short, non-reversible fingerprint for the key without logging raw
- * key bytes. Non-extractable keys fall back to algorithm and type.
+ * Computes a short, non-reversible fingerprint for a public key. Private and
+ * secret keys are summarized only by algorithm and type, and their encodings
+ * are never requested.
*
* @param key the key to summarize; may be null
* @return a fingerprint string or "n/a" if the key is null
@@ -639,30 +585,34 @@ public final class JulAuditListenerStd implements AuditListener {
if (key == null) {
return "n/a";
}
+ if (!(key instanceof PublicKey)) {
+ return keyType(key);
+ }
+ byte[] enc = null;
+ byte[] digest = null;
try {
- byte[] enc = key.getEncoded();
+ enc = key.getEncoded();
if (enc == null) {
return key.getAlgorithm() + ":" + key.getClass().getSimpleName();
}
MessageDigest md = MessageDigest.getInstance("SHA-256");
- byte[] d = md.digest(enc);
+ digest = md.digest(enc);
StringBuilder sb = new StringBuilder(key.getAlgorithm()).append(':');
- for (int i = 0; i < Math.min(8, d.length); i++) {
- sb.append(String.format("%02x", d[i]));
+ for (int i = 0; i < Math.min(8, digest.length); i++) {
+ int value = digest[i] & 0xff;
+ sb.append(Character.forDigit(value >>> 4, 16))
+ .append(Character.forDigit(value & 0x0f, 16));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
return key.getAlgorithm() + ":fp-error";
+ } finally {
+ if (enc != null) {
+ Arrays.fill(enc, (byte) 0);
+ }
+ if (digest != null) {
+ Arrays.fill(digest, (byte) 0);
+ }
}
}
-
- /**
- * Returns a non-null message string for logging.
- *
- * @param s a message, possibly null
- * @return the message or an empty string if null
- */
- private static String safeMessage(String s) {
- return s == null ? "" : s;
- }
}
diff --git a/lib/src/main/java/zeroecho/core/context/AgreementContext.java b/lib/src/main/java/zeroecho/core/context/AgreementContext.java
index 7d2f77b..834c0b0 100644
--- a/lib/src/main/java/zeroecho/core/context/AgreementContext.java
+++ b/lib/src/main/java/zeroecho/core/context/AgreementContext.java
@@ -49,8 +49,8 @@ import java.security.PublicKey;
*
* Lifecycle
*
- * - Create a context via {@code CryptoAlgorithm#create(...)} or
- * {@code CryptoAlgorithms.create(...)} for the {@code AGREEMENT} role.
+ * - Create a context via {@code ZeroEchoSession#createContext(...)} for the
+ * {@code AGREEMENT} role.
* - Call {@link #setPeerPublic(PublicKey)} with the peer’s public key.
* - Invoke {@link #deriveSecret()} once to compute the raw shared
* secret.
diff --git a/lib/src/main/java/zeroecho/core/context/CryptoContext.java b/lib/src/main/java/zeroecho/core/context/CryptoContext.java
index 8392e3b..64090c3 100644
--- a/lib/src/main/java/zeroecho/core/context/CryptoContext.java
+++ b/lib/src/main/java/zeroecho/core/context/CryptoContext.java
@@ -52,8 +52,7 @@ import zeroecho.core.CryptoAlgorithm;
*
* Lifecycle
*
- * - Contexts are created via {@code CryptoAlgorithm#create(...)} or the
- * convenience methods in {@code CryptoAlgorithms}.
+ * - Contexts are created via {@code ZeroEchoSession#createContext(...)}.
* - They may wrap native or provider-managed resources that must be
* released.
* - Once closed, a context must not be reused; callers should request a new
@@ -62,9 +61,8 @@ import zeroecho.core.CryptoAlgorithm;
*
*
Security considerations
*
- * - Closing a context may attempt to destroy the underlying {@link Key} if it
- * implements {@code javax.security.auth.Destroyable} and auditing is
- * enabled.
+ * - Bound keys are borrowed from the caller and are never destroyed by
+ * context closure.
* - Applications should always call {@link #close()} promptly to avoid
* leaking key material or other sensitive state.
* - Contexts are not guaranteed to be thread-safe; concurrent use should be
@@ -110,9 +108,10 @@ public sealed interface CryptoContext extends Closeable
* Closes this context and releases all associated resources.
*
*
- * Implementations should free provider state and native handles. If the bound
- * key supports destruction, the library may attempt to invoke {@code destroy()}
- * on it when auditing is enabled.
+ * Implementations free context-owned provider state and native handles. The
+ * bound key remains caller-owned: closing a context never destroys the key.
+ * Callers that own a destroyable key must destroy it explicitly after all
+ * contexts and other consumers have released it.
*
*
* @throws java.io.IOException if the underlying provider encounters an I/O
diff --git a/lib/src/main/java/zeroecho/core/context/EncryptionContext.java b/lib/src/main/java/zeroecho/core/context/EncryptionContext.java
index c5f5bf1..088f047 100644
--- a/lib/src/main/java/zeroecho/core/context/EncryptionContext.java
+++ b/lib/src/main/java/zeroecho/core/context/EncryptionContext.java
@@ -48,7 +48,7 @@ import java.io.InputStream;
*
* Lifecycle
*
- * - Create a context via {@code CryptoAlgorithm#create(...)} for the
+ *
- Create a context via {@code ZeroEchoSession#createContext(...)} for the
* {@link zeroecho.core.KeyUsage#ENCRYPT} or
* {@link zeroecho.core.KeyUsage#DECRYPT} role.
* - Call {@link #attach(InputStream)} to obtain a wrapped stream.
diff --git a/lib/src/main/java/zeroecho/core/context/KemContext.java b/lib/src/main/java/zeroecho/core/context/KemContext.java
index 11c343d..215df52 100644
--- a/lib/src/main/java/zeroecho/core/context/KemContext.java
+++ b/lib/src/main/java/zeroecho/core/context/KemContext.java
@@ -46,7 +46,7 @@ import java.io.IOException;
*
* Lifecycle
*
- * - Create a context via {@code CryptoAlgorithm#create(...)} for the
+ *
- Create a context via {@code ZeroEchoSession#createContext(...)} for the
* {@link zeroecho.core.KeyUsage#ENCAPSULATE} or
* {@link zeroecho.core.KeyUsage#DECAPSULATE} role.
* - Call {@link #encapsulate()} when acting as an initiator. The result
diff --git a/lib/src/main/java/zeroecho/core/context/MessageAgreementContext.java b/lib/src/main/java/zeroecho/core/context/MessageAgreementContext.java
index 5b65dcf..8044c06 100644
--- a/lib/src/main/java/zeroecho/core/context/MessageAgreementContext.java
+++ b/lib/src/main/java/zeroecho/core/context/MessageAgreementContext.java
@@ -57,7 +57,7 @@ package zeroecho.core.context;
*
*
Lifecycle
*
- * - Create a context via {@code CryptoAlgorithm#create(...)} for the
+ *
- Create a context via {@code ZeroEchoSession#createContext(...)} for the
* {@link zeroecho.core.KeyUsage#AGREEMENT} role.
* - Initiators call {@link #getPeerMessage()} to obtain the message to send
* to the responder, then invoke {@link AgreementContext#deriveSecret()}.
diff --git a/lib/src/main/java/zeroecho/core/err/UnsupportedRoleException.java b/lib/src/main/java/zeroecho/core/err/UnsupportedRoleException.java
index c5dfd25..3711286 100644
--- a/lib/src/main/java/zeroecho/core/err/UnsupportedRoleException.java
+++ b/lib/src/main/java/zeroecho/core/err/UnsupportedRoleException.java
@@ -46,11 +46,13 @@ package zeroecho.core.err;
* When it is thrown
*
* - During
- * {@link zeroecho.core.CryptoAlgorithms#create(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}
+ * {@link zeroecho.sdk.ZeroEchoSession#createContext(String,
+ * zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}
* after policy validation, if the resolved algorithm exposes no bindings for
* the given role.
* - Directly from
- * {@link zeroecho.core.CryptoAlgorithm#create(zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}
+ * {@link zeroecho.core.CryptoAlgorithm#createContext(zeroecho.core.KeyUsage,
+ * java.security.Key, zeroecho.core.spec.ContextSpec)}
* when no binding exists for the role.
*
*
@@ -61,10 +63,10 @@ package zeroecho.core.err;
*
* Example
{@code
* // Suppose "SHA-256" supports DIGEST only.
- * var algo = zeroecho.core.CryptoAlgorithms.require("SHA-256");
+ * zeroecho.sdk.ZeroEchoSession session = new zeroecho.sdk.ZeroEchoSession();
* try {
* // Asking for ENCRYPT on a digest algorithm will fail with UnsupportedRoleException.
- * zeroecho.core.CryptoAlgorithms.create("SHA-256", zeroecho.core.KeyUsage.ENCRYPT,
+ * session.createContext("SHA-256", zeroecho.core.KeyUsage.ENCRYPT,
* zeroecho.core.NullKey.INSTANCE, null);
* } catch (UnsupportedRoleException e) {
* // Handle: algorithm does not implement the ENCRYPT role.
diff --git a/lib/src/main/java/zeroecho/core/err/UnsupportedSpecException.java b/lib/src/main/java/zeroecho/core/err/UnsupportedSpecException.java
index 7267438..7b2f269 100644
--- a/lib/src/main/java/zeroecho/core/err/UnsupportedSpecException.java
+++ b/lib/src/main/java/zeroecho/core/err/UnsupportedSpecException.java
@@ -38,7 +38,9 @@ package zeroecho.core.err;
* the algorithm for the requested role.
*
*
- * This is thrown by {@link zeroecho.core.CryptoAlgorithm#create} when:
+ * This is thrown by
+ * {@link zeroecho.core.CryptoAlgorithm#createContext(zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}
+ * when:
*
*
* - The algorithm supports the requested {@link zeroecho.core.KeyUsage} role,
@@ -58,7 +60,7 @@ package zeroecho.core.err;
* CryptoAlgorithm aes = CryptoAlgorithms.require("AES/GCM");
* SecretKey wrongKey = ... // an RSA key by mistake
* try {
- * aes.create(KeyUsage.ENCRYPT, wrongKey, null);
+ * aes.createContext(KeyUsage.ENCRYPT, wrongKey, null);
* } catch (UnsupportedSpecException e) {
* // no binding accepted the RSA key for ENCRYPT role
* }
diff --git a/lib/src/main/java/zeroecho/core/err/package-info.java b/lib/src/main/java/zeroecho/core/err/package-info.java
index fabb46a..1f4c955 100644
--- a/lib/src/main/java/zeroecho/core/err/package-info.java
+++ b/lib/src/main/java/zeroecho/core/err/package-info.java
@@ -67,7 +67,7 @@
*
Typical usage
{@code
* try {
* zeroecho.core.context.EncryptionContext ctx =
- * algo.create(zeroecho.core.KeyUsage.ENCRYPT, key, spec);
+ * algo.createContext(zeroecho.core.KeyUsage.ENCRYPT, key, spec);
* // use ctx...
* } catch (zeroecho.core.err.UnsupportedRoleException
* | zeroecho.core.err.UnsupportedSpecException e) {
diff --git a/lib/src/main/java/zeroecho/core/io/AbstractChunkTransformInputStream.java b/lib/src/main/java/zeroecho/core/io/AbstractChunkTransformInputStream.java
index f2abfdc..bf3088b 100644
--- a/lib/src/main/java/zeroecho/core/io/AbstractChunkTransformInputStream.java
+++ b/lib/src/main/java/zeroecho/core/io/AbstractChunkTransformInputStream.java
@@ -123,6 +123,7 @@ import java.io.InputStream;
* @since 1.0
*/
public abstract class AbstractChunkTransformInputStream extends FilterInputStream {
+ private static final int MIN_INPUT_CHUNK_SIZE = 2;
/** Input buffer storing data read from the upstream stream. */
protected final byte[] inBuf;
/** Output buffer storing transformed bytes awaiting consumption. */
@@ -156,15 +157,14 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
* @param outChunkSize size of output chunks produced by the transform (must be
* > 0)
* @param chunks number of chunks buffered at once (must be > 0)
- * @throws AssertionError if {@code chunks <= 0}, {@code inChunkSize <= 1}, or
- * {@code outChunkSize <= 0}
+ * @throws IllegalArgumentException if a size is outside its documented range
+ * or a buffer size overflows
*/
protected AbstractChunkTransformInputStream(InputStream upstream, int inChunkSize, int outChunkSize, int chunks) {
super(upstream);
- assert chunks > 0 && inChunkSize > 1 && outChunkSize > 0;
-
- this.inBuf = new byte[inChunkSize * chunks];
- this.outBuf = new byte[outChunkSize * chunks];
+ validateGeometry(inChunkSize, outChunkSize, chunks, 0);
+ this.inBuf = new byte[checkedMultiply(inChunkSize, chunks, "input buffer size")];
+ this.outBuf = new byte[checkedMultiply(outChunkSize, chunks, "output buffer size")];
this.inChunkSize = inChunkSize;
this.outChunkSize = outChunkSize;
@@ -190,16 +190,16 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
* steady-state (must be > 0)
* @param finalizationOutputChunks number of extra output chunks reserved for
* finalization (must be >= 0)
- * @throws AssertionError if {@code chunks <= 0}, {@code inChunkSize <= 1}, or
- * {@code outChunkSize <= 0}
+ * @throws IllegalArgumentException if a size is outside its documented range
+ * or a buffer size overflows
*/
protected AbstractChunkTransformInputStream(InputStream upstream, int inChunkSize, int outChunkSize, int chunks,
int finalizationOutputChunks) {
super(upstream);
- assert chunks > 0 && inChunkSize > 1 && outChunkSize > 0;
-
- this.inBuf = new byte[inChunkSize * chunks];
- this.outBuf = new byte[outChunkSize * (chunks + finalizationOutputChunks)];
+ validateGeometry(inChunkSize, outChunkSize, chunks, finalizationOutputChunks);
+ int outputChunks = checkedAdd(chunks, finalizationOutputChunks, "output chunk count");
+ this.inBuf = new byte[checkedMultiply(inChunkSize, chunks, "input buffer size")];
+ this.outBuf = new byte[checkedMultiply(outChunkSize, outputChunks, "output buffer size")];
this.inChunkSize = inChunkSize;
this.outChunkSize = outChunkSize;
@@ -252,14 +252,17 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
* @throws IOException if the upstream read or the transformation fails
*/
private boolean fillBuffers() throws IOException {
- assert outPtr == outLen;
+ if (outPtr != outLen) {
+ throw new IllegalStateException("Output buffer was refilled before it was drained");
+ }
inLen = in.readNBytes(inBuf, 0, inBuf.length);
if (inLen == 0) {
// EOF: run finalization exactly once, even if there's no remainder,
// and surface any produced bytes (e.g., padding block, GCM tag).
if (!eofSeen) {
- int finalOut = doFinal(inBuf, 0, 0, outBuf, 0);
+ int finalOut = validateOutputCount(doFinal(inBuf, 0, 0, outBuf, 0), outBuf.length,
+ "finalization");
outPtr = 0;
outLen = finalOut;
eofSeen = true;
@@ -270,14 +273,16 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
// all chunks are aligned to the specified boundary (inChunkSize) -> transform
// can be simply invoked
- outLen = transform(inBuf, 0, inLen / inChunkSize, outBuf);
+ outLen = validateOutputCount(transform(inBuf, 0, inLen / inChunkSize, outBuf), outBuf.length,
+ "transformation");
outPtr = 0;
int left = inLen % inChunkSize;
if (left > 0) {
int finalOutChunkSize = doFinal(inBuf, inLen - left, left, outBuf, outLen);
- outLen = outLen + finalOutChunkSize;
+ finalOutChunkSize = validateOutputCount(finalOutChunkSize, outBuf.length - outLen, "finalization");
+ outLen = checkedAddState(outLen, finalOutChunkSize);
// we ask for whole inBufSize chunks, if readNBytes returns a partial chunk, it
// must be eof
eofSeen = true;
@@ -286,6 +291,53 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
return true;
}
+ private static void validateGeometry(int inChunkSize, int outChunkSize, int chunks,
+ int finalizationOutputChunks) {
+ if (inChunkSize < MIN_INPUT_CHUNK_SIZE) {
+ throw new IllegalArgumentException("inChunkSize must be greater than 1");
+ }
+ if (outChunkSize <= 0) {
+ throw new IllegalArgumentException("outChunkSize must be greater than 0");
+ }
+ if (chunks <= 0) {
+ throw new IllegalArgumentException("chunks must be greater than 0");
+ }
+ if (finalizationOutputChunks < 0) {
+ throw new IllegalArgumentException("finalizationOutputChunks must not be negative");
+ }
+ }
+
+ private static int checkedMultiply(int left, int right, String description) {
+ try {
+ return Math.multiplyExact(left, right);
+ } catch (ArithmeticException exception) {
+ throw new IllegalArgumentException(description + " exceeds the supported range", exception);
+ }
+ }
+
+ private static int checkedAdd(int left, int right, String description) {
+ try {
+ return Math.addExact(left, right);
+ } catch (ArithmeticException exception) {
+ throw new IllegalArgumentException(description + " exceeds the supported range", exception);
+ }
+ }
+
+ private static int checkedAddState(int left, int right) {
+ try {
+ return Math.addExact(left, right);
+ } catch (ArithmeticException exception) {
+ throw new IllegalStateException("Transform output length overflowed", exception);
+ }
+ }
+
+ private static int validateOutputCount(int count, int capacity, String operation) {
+ if (count < 0 || count > capacity) {
+ throw new IllegalStateException(operation + " returned an invalid output count: " + count);
+ }
+ return count;
+ }
+
/**
* Reads the next transformed byte.
*
@@ -295,11 +347,12 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
*/
@Override
public int read() throws IOException {
- if (outPtr < outLen) {
- return outBuf[outPtr++] & 0xff;
+ while (outPtr >= outLen) {
+ if (!fillBuffers()) {
+ return -1;
+ }
}
-
- return fillBuffers() ? outBuf[outPtr++] & 0xff : -1 /* eof */;
+ return outBuf[outPtr++] & 0xff;
}
/**
diff --git a/lib/src/main/java/zeroecho/core/io/CipherTransformInputStreamBuilder.java b/lib/src/main/java/zeroecho/core/io/CipherTransformInputStreamBuilder.java
index 04845e9..c1ca971 100644
--- a/lib/src/main/java/zeroecho/core/io/CipherTransformInputStreamBuilder.java
+++ b/lib/src/main/java/zeroecho/core/io/CipherTransformInputStreamBuilder.java
@@ -48,15 +48,16 @@ import javax.crypto.Cipher;
* {@link Cipher}. Three stream variants are available:
*
*
- * - SmartBlockStream - invokes
+ *
- Independent block stream - invokes
* {@link Cipher#doFinal(byte[], int, int, byte[], int)} for each full input
* block; a final partial block (if any) is processed by a single
- * {@code doFinal}.
- * - SmartPaddedBlockStream - like {@code SmartBlockStream}, but
+ * {@code doFinal}. This mode is restricted to RSA and ElGamal.
+ * - Left-padded independent block stream - like the independent block
+ * stream, but
* left-pads each transformed output block with zeros up to
* {@code outChunkSize}. Final blocks must be complete; otherwise an
* {@link IllegalStateException} is thrown.
- * - SmartContinuousBlockStream - streaming variant that uses
+ *
- Continuous stream - uses
* {@code Cipher.update(...)} for bulk bytes and a single {@code doFinal()} at
* end of stream. This is suitable for CTR/CFB/OFB/GCM and padding modes.
*
@@ -128,7 +129,7 @@ import javax.crypto.Cipher;
* InputStream s4 = CipherTransformInputStreamBuilder.builder()
* .withUpstream(in)
* .withCipher(c4)
- * .withUpdateStreaming(true) // provider typically accepts update+doFinal
+ * .withIndependentBlocks()
* .withInputBlockSize(elgIn)
* .withOutputBlockSize(elgOut)
* .withBufferedBlocks(200)
@@ -313,6 +314,24 @@ public final class CipherTransformInputStreamBuilder {
return this;
}
+ /**
+ * Selects independent-block processing, in which every logical input block is
+ * passed to a separate {@link Cipher#doFinal(byte[], int, int, byte[], int)}
+ * invocation.
+ *
+ *
+ * This mode is supported only for RSA and ElGamal transformations. Stateful
+ * symmetric modes, including AES-GCM and AES-CBC, must use
+ * {@link #withUpdateStreaming()}.
+ *
+ *
+ * @return this builder
+ */
+ public CipherTransformInputStreamBuilder withIndependentBlocks() {
+ this.updateStreaming = false;
+ return this;
+ }
+
/**
* Builds a chunk-transforming {@link InputStream} using the configured options.
*
@@ -326,7 +345,9 @@ public final class CipherTransformInputStreamBuilder {
*
* @return a new InputStream that transforms bytes on the fly
* @throws NullPointerException if {@code upstream} or {@code cipher} is null
- * @throws AssertionError if buffer sizing assertions fail
+ * @throws IllegalArgumentException if independent-block processing is selected
+ * for an unsupported algorithm or buffer
+ * geometry is invalid
*/
public InputStream build() {
Objects.requireNonNull(upstream, "upstream must not be null");
@@ -336,7 +357,18 @@ public final class CipherTransformInputStreamBuilder {
return new SmartContinuousBlockStream(upstream, cipher, inChunkSize, outChunkSize, bufferedBlocks,
finalizationOutputChunks);
}
+ validateIndependentBlockAlgorithm(cipher);
return padding ? new SmartPaddedBlockStream(upstream, cipher, inChunkSize, outChunkSize, bufferedBlocks)
: new SmartBlockStream(upstream, cipher, inChunkSize, outChunkSize, bufferedBlocks);
}
+
+ private static void validateIndependentBlockAlgorithm(Cipher cipher) {
+ String transformation = cipher.getAlgorithm();
+ int separator = transformation.indexOf('/');
+ String baseAlgorithm = separator < 0 ? transformation : transformation.substring(0, separator);
+ if (!"RSA".equalsIgnoreCase(baseAlgorithm) && !"ElGamal".equalsIgnoreCase(baseAlgorithm)) {
+ throw new IllegalArgumentException(
+ "Independent-block processing supports only RSA and ElGamal transformations");
+ }
+ }
}
diff --git a/lib/src/main/java/zeroecho/core/io/SmartBlockStream.java b/lib/src/main/java/zeroecho/core/io/SmartBlockStream.java
index f0fa9b9..876c195 100644
--- a/lib/src/main/java/zeroecho/core/io/SmartBlockStream.java
+++ b/lib/src/main/java/zeroecho/core/io/SmartBlockStream.java
@@ -58,8 +58,6 @@ final class SmartBlockStream extends AbstractChunkTransformInputStream {
private static final Logger LOG = Logger.getLogger(SmartBlockStream.class.getName());
private final Cipher cipher;
- private boolean doFinalCalled;
-
/* package */ SmartBlockStream(InputStream upstream, Cipher cipher, int inChunkSize, int outChunkSize,
int bufferedBlocks) {
super(upstream, inChunkSize, outChunkSize, bufferedBlocks);
@@ -81,7 +79,6 @@ final class SmartBlockStream extends AbstractChunkTransformInputStream {
protected int transform(byte[] in, int inOff, int inChunks, byte[] out) throws IOException {
try {
int output = 0;
- doFinalCalled = inChunks > 0;
for (int i = 0; i < inChunks; i++) {
int outOne = cipher.doFinal(in, inOff, inChunkSize, out, output);
output = output + outOne;
@@ -108,11 +105,10 @@ final class SmartBlockStream extends AbstractChunkTransformInputStream {
@Override
protected int doFinal(byte[] in, int inOff, int len, byte[] out, int outOff) throws IOException {
try {
- if (doFinalCalled && len == 0) {
+ if (len == 0) {
return 0;
}
- doFinalCalled = true;
return cipher.doFinal(in, inOff, len, out, outOff);
} catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException e) {
LOG.logp(Level.WARNING, "SmartBlockStream", "transform", "Exception", e);
diff --git a/lib/src/main/java/zeroecho/core/io/SmartContinuousBlockStream.java b/lib/src/main/java/zeroecho/core/io/SmartContinuousBlockStream.java
index cb35c78..7dd8b19 100644
--- a/lib/src/main/java/zeroecho/core/io/SmartContinuousBlockStream.java
+++ b/lib/src/main/java/zeroecho/core/io/SmartContinuousBlockStream.java
@@ -157,12 +157,18 @@ final class SmartContinuousBlockStream extends AbstractChunkTransformInputStream
protected int doFinal(byte[] in, int inOff, int len, byte[] out, int outOff) throws IOException {
try {
int finBlockSize = cipher.getOutputSize(len);
- if (out.length < outOff + finBlockSize) {
+ final int required;
+ try {
+ required = Math.addExact(outOff, finBlockSize);
+ } catch (ArithmeticException exception) {
+ throw new IOException("Final cipher output size exceeds the supported range", exception);
+ }
+ if (out.length < required) {
if (LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Expanding buffer of {0} from {1} bytes to {2} bytes",
- new Object[] { cipher.getAlgorithm(), outBuf.length, outOff + finBlockSize });
+ new Object[] { cipher.getAlgorithm(), outBuf.length, required });
}
- out = outBuf = Arrays.copyOf(outBuf, outOff + finBlockSize); // NOPMD
+ out = outBuf = Arrays.copyOf(outBuf, required); // NOPMD
}
int written = cipher.doFinal(in, inOff, len, out, outOff);
diff --git a/lib/src/main/java/zeroecho/core/io/SmartPaddedBlockStream.java b/lib/src/main/java/zeroecho/core/io/SmartPaddedBlockStream.java
index 34424b0..0087cbe 100644
--- a/lib/src/main/java/zeroecho/core/io/SmartPaddedBlockStream.java
+++ b/lib/src/main/java/zeroecho/core/io/SmartPaddedBlockStream.java
@@ -59,8 +59,6 @@ final class SmartPaddedBlockStream extends AbstractChunkTransformInputStream {
private static final Logger LOG = Logger.getLogger(SmartPaddedBlockStream.class.getName());
private final Cipher cipher;
- private boolean doFinalCalled;
-
/* package */ SmartPaddedBlockStream(InputStream upstream, Cipher cipher, int inChunkSize, int outChunkSize,
int bufferedBlocks) {
super(upstream, inChunkSize, outChunkSize, bufferedBlocks);
@@ -83,9 +81,11 @@ final class SmartPaddedBlockStream extends AbstractChunkTransformInputStream {
try {
// return cipher.doFinal(in, inOff, inChunks * g.inputBlockSize(), out);
int output = 0;
- doFinalCalled = inChunks > 0;
for (int i = 0; i < inChunks; i++) {
int outOne = cipher.doFinal(in, inOff, inChunkSize, out, output);
+ if (outOne < 0 || outOne > outChunkSize) {
+ throw new IOException("Cipher output exceeds the configured block size");
+ }
int diff = outChunkSize - outOne;
if (diff > 0) {
System.arraycopy(out, output, out, output + diff, outOne);
@@ -117,7 +117,7 @@ final class SmartPaddedBlockStream extends AbstractChunkTransformInputStream {
*/
@Override
protected int doFinal(byte[] in, int inOff, int len, byte[] out, int outOff) throws IOException {
- if (doFinalCalled && len == 0) {
+ if (len == 0) {
return 0;
}
@@ -125,7 +125,6 @@ final class SmartPaddedBlockStream extends AbstractChunkTransformInputStream {
throw new IllegalStateException("Cannot process incomplete blocks: " + len + " instead of " + inChunkSize);
}
try {
- doFinalCalled = true;
return cipher.doFinal(in, inOff, len, out, outOff);
} catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException e) {
LOG.logp(Level.WARNING, "SmartBlockStream", "transform", "Exception", e);
diff --git a/lib/src/main/java/zeroecho/core/io/Util.java b/lib/src/main/java/zeroecho/core/io/Util.java
index fc1599a..67b8819 100644
--- a/lib/src/main/java/zeroecho/core/io/Util.java
+++ b/lib/src/main/java/zeroecho/core/io/Util.java
@@ -79,6 +79,8 @@ public final class Util { // NOPMD
*
*/
private static final int DEFAULT_BUFFER_SIZE = 32 * 1024;
+ /** Largest unsigned 32-bit value accepted by the packed integer decoder. */
+ private static final long MAX_PACKED_INTEGER = 0xffff_ffffL;
/**
* Private constructor to prevent instantiation of this utility class.
@@ -271,15 +273,29 @@ public final class Util { // NOPMD
* @throws IOException if an I/O error occurs or if the stream ends prematurely
*/
public static int readPack7I(final InputStream in) throws IOException {
- int result = in.read();
- if (result > 0x7f) { // NOPMD
- return result & 0x7f;
+ int current = in.read();
+ if (current < 0) {
+ throw new EOFException("read packed integer EOF");
}
- int i;
- for (i = in.read(); i < 0x80; i = in.read()) {
- result = (result << 7) | i;
+ if (current > 0x7f) { // NOPMD
+ return current & 0x7f;
}
- return (result << 7) | (i & 0x7f);
+
+ long result = current;
+ for (int bytes = 1; bytes < 5; bytes++) {
+ current = in.read();
+ if (current < 0) {
+ throw new EOFException("read packed integer EOF");
+ }
+ result = (result << 7) | (current & 0x7f);
+ if (current > 0x7f) { // NOPMD
+ if (result > MAX_PACKED_INTEGER) {
+ throw new IOException("packed integer exceeds 32 bits");
+ }
+ return (int) result;
+ }
+ }
+ throw new IOException("packed integer exceeds five bytes");
}
/**
diff --git a/lib/src/main/java/zeroecho/core/io/package-info.java b/lib/src/main/java/zeroecho/core/io/package-info.java
index 3f8fcde..09ed79a 100644
--- a/lib/src/main/java/zeroecho/core/io/package-info.java
+++ b/lib/src/main/java/zeroecho/core/io/package-info.java
@@ -53,10 +53,12 @@
* invokes {@code update(...)} on each chunk, optionally emits a single trailer,
* then calls {@code onCompleted()} exactly once at EOF.
* - {@link CipherTransformInputStreamBuilder} - fluent builder that creates
- * cipher-backed streams for block-per-doFinal, left-zero-padded blocks, or
+ * cipher-backed streams for RSA/ElGamal independent-block processing,
+ * left-zero-padded independent blocks, or
* continuous {@code update}+{@code doFinal} streaming.
* - {@link SmartBlockStream}, {@link SmartPaddedBlockStream},
- * {@link SmartContinuousBlockStream} - concrete cipher-backed stream variants
+ * {@link SmartContinuousBlockStream} - internal cipher-backed stream variants;
+ * the first two are restricted to independent RSA or ElGamal blocks
* used by the builder.
* - {@link TailStrippingInputStream} - withholds the last N bytes from the
* payload and delivers them to a callback at EOF (useful for tags, checksums,
diff --git a/lib/src/main/java/zeroecho/core/marshal/PairSeq.java b/lib/src/main/java/zeroecho/core/marshal/PairSeq.java
index 35bd702..b0a72fd 100644
--- a/lib/src/main/java/zeroecho/core/marshal/PairSeq.java
+++ b/lib/src/main/java/zeroecho/core/marshal/PairSeq.java
@@ -36,7 +36,6 @@ package zeroecho.core.marshal;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
-import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
@@ -62,8 +61,8 @@ import java.util.List;
*
*
Serialization
*
- * - {@link #writeTo(Appendable)} outputs each pair as {@code k=v\n} lines
- * without escaping.
+ * - {@link #writeTo(Appendable)} outputs each pair as {@code k=v\n}
+ * lines without escaping and reports checked I/O failures.
* - {@link #readFrom(java.io.Reader)} parses lines in the same format,
* ignoring blank lines and comments starting with {@code #}.
*
@@ -86,8 +85,8 @@ public final class PairSeq {
*
* @param kv alternating key and value strings; must have even length
* @return new {@code PairSeq} with the given contents
- * @throws IllegalArgumentException if {@code kv} is {@code null} or has odd
- * length
+ * @throws IllegalArgumentException if {@code kv} is {@code null}, has odd
+ * length, or contains a null key or value
*/
public static PairSeq of(String... kv) {
if (kv == null) {
@@ -96,7 +95,15 @@ public final class PairSeq {
if ((kv.length & 1) != 0) {
throw new IllegalArgumentException("kv must have even length (k,v pairs)");
}
- return new PairSeq(kv);
+ for (int elementIndex = 0; elementIndex < kv.length; elementIndex++) {
+ if (kv[elementIndex] == null) {
+ int pairIndex = elementIndex >>> 1;
+ String role = (elementIndex & 1) == 0 ? "key" : "value";
+ throw new IllegalArgumentException(
+ "pair " + pairIndex + " " + role + " must not be null");
+ }
+ }
+ return new PairSeq(kv.clone());
}
/**
@@ -194,7 +201,8 @@ public final class PairSeq {
}
/**
- * Appends all pairs to the target as {@code key=value} lines.
+ * Appends all pairs to the target as {@code key=value} lines, reporting
+ * checked I/O failures directly.
*
*
* No escaping is performed; callers must ensure keys and values do not contain
@@ -202,15 +210,11 @@ public final class PairSeq {
*
*
* @param out appendable target
- * @throws UncheckedIOException if the append fails
+ * @throws IOException if the append fails
*/
- public void writeTo(Appendable out) {
- try {
- for (int i = 0; i < size(); i++) {
- out.append(keyAt(i)).append('=').append(valAt(i)).append('\n');
- }
- } catch (IOException e) {
- throw new UncheckedIOException(e);
+ public void writeTo(Appendable out) throws IOException {
+ for (int i = 0; i < size(); i++) {
+ out.append(keyAt(i)).append('=').append(valAt(i)).append('\n');
}
}
@@ -244,6 +248,6 @@ public final class PairSeq {
list.add(k);
list.add(v);
}
- return new PairSeq(list.toArray(String[]::new));
+ return of(list.toArray(String[]::new));
}
}
diff --git a/lib/src/main/java/zeroecho/core/marshal/PairSeqCodec.java b/lib/src/main/java/zeroecho/core/marshal/PairSeqCodec.java
index 54f9d6f..c6e280f 100644
--- a/lib/src/main/java/zeroecho/core/marshal/PairSeqCodec.java
+++ b/lib/src/main/java/zeroecho/core/marshal/PairSeqCodec.java
@@ -33,10 +33,15 @@
******************************************************************************/
package zeroecho.core.marshal;
-import java.lang.reflect.Constructor;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Supplier;
/**
* Reflection-based {@link Codec} that marshals and unmarshals domain objects to
@@ -95,14 +100,29 @@ import java.util.Objects;
* User u = codec.unmarshal(repr);
* }
*
- * Thread-safety
Instances are immutable and thread-safe. Reflection
- * lookups are performed per call and are not cached.
+ * Thread-safety
Instances are immutable and thread-safe. Public
+ * accessors are resolved once per runtime class and operation type, then invoked
+ * through cached method handles. The unload-safe {@link ClassValue} caches do
+ * not retain otherwise unreachable class loaders.
*
* @param domain type that follows the marshalling and unmarshalling
* conventions
* @since 1.0
*/
public final class PairSeqCodec implements Codec {
+ private static final ClassValue> MARSHAL_PLANS = new ClassValue<>() {
+ @Override
+ protected PlanHolder computeValue(Class> type) {
+ return new PlanHolder<>(() -> MarshalPlan.resolve(type));
+ }
+ };
+ private static final ClassValue> UNMARSHAL_PLANS = new ClassValue<>() {
+ @Override
+ protected PlanHolder computeValue(Class> type) {
+ return new PlanHolder<>(() -> UnmarshalPlan.resolve(type));
+ }
+ };
+
private final Class type;
/**
@@ -136,17 +156,7 @@ public final class PairSeqCodec implements Codec {
@Override
public PairSeq marshal(T value) {
Objects.requireNonNull(value, "value");
- try {
- Method m = value.getClass().getMethod("marshal");
- if (!PairSeq.class.isAssignableFrom(m.getReturnType())) {
- throw new IllegalStateException("marshal() must return PairSeq in " + value.getClass().getName());
- }
- return (PairSeq) m.invoke(value);
- } catch (NoSuchMethodException e) {
- throw new IllegalStateException(value.getClass().getName() + " must implement marshal():PairSeq", e);
- } catch (IllegalAccessException | InvocationTargetException t) {
- throw new IllegalStateException("marshal() failed for " + value.getClass().getName(), t);
- }
+ return MARSHAL_PLANS.get(value.getClass()).get().invoke(value);
}
/**
@@ -174,31 +184,181 @@ public final class PairSeqCodec implements Codec {
* constructor exists, or if either invocation
* fails
*/
- @SuppressWarnings("unchecked")
@Override
public T unmarshal(PairSeq repr) {
Objects.requireNonNull(repr, "repr");
- // Prefer static unmarshal(PairSeq)
- try {
- Method m = type.getMethod("unmarshal", PairSeq.class);
- if ((m.getModifiers() & java.lang.reflect.Modifier.STATIC) != 0) {
- return (T) m.invoke(null, repr);
- }
- } catch (NoSuchMethodException ignore) { // NOPMD
- // fall through
- } catch (IllegalAccessException | InvocationTargetException t) {
- throw new IllegalStateException("static unmarshal(PairSeq) failed for " + type.getName(), t);
+ return type.cast(UNMARSHAL_PLANS.get(type).get().invoke(repr));
+ }
+
+ /* default */ static Object cachedMarshalPlan(Class> runtimeType) {
+ return MARSHAL_PLANS.get(runtimeType).get();
+ }
+
+ /* default */ static Object cachedUnmarshalPlan(Class> runtimeType) {
+ return UNMARSHAL_PLANS.get(runtimeType).get();
+ }
+
+ /* default */ static int marshalResolutionCount(Class> runtimeType) {
+ return MARSHAL_PLANS.get(runtimeType).resolutionCount();
+ }
+
+ /* default */ static int unmarshalResolutionCount(Class> runtimeType) {
+ return UNMARSHAL_PLANS.get(runtimeType).resolutionCount();
+ }
+
+ /**
+ * Once-only lazy plan resolver stored as the canonical {@link ClassValue}
+ * value.
+ *
+ * @param plan type
+ */
+ private static final class PlanHolder
{
+ private final AtomicReference
plan = new AtomicReference<>();
+ private final ReentrantLock resolutionLock = new ReentrantLock();
+ private Supplier
resolver;
+ private int resolutionCount;
+
+ private PlanHolder(Supplier
resolver) {
+ this.resolver = resolver;
}
- // Or constructor T(PairSeq)
- try {
- Constructor c = type.getConstructor(PairSeq.class);
- return c.newInstance(repr);
- } catch (NoSuchMethodException e) {
- throw new IllegalStateException(type.getName() + " must provide static unmarshal(PairSeq) or ctor(PairSeq)",
- e);
- } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
- | InstantiationException t) {
- throw new IllegalStateException("ctor(PairSeq) failed for " + type.getName(), t);
+
+ private P get() {
+ P resolved = plan.get();
+ if (resolved == null) {
+ resolved = resolveOnce();
+ }
+ return resolved;
+ }
+
+ private P resolveOnce() {
+ resolutionLock.lock();
+ try {
+ P resolved = plan.get();
+ if (resolved == null) {
+ resolutionCount++;
+ resolved = resolver.get();
+ resolver = null;
+ plan.set(resolved);
+ }
+ return resolved;
+ } finally {
+ resolutionLock.unlock();
+ }
+ }
+
+ private int resolutionCount() {
+ resolutionLock.lock();
+ try {
+ return resolutionCount;
+ } finally {
+ resolutionLock.unlock();
+ }
+ }
+ }
+
+ /**
+ * Cached success or structural failure for one marshal runtime class.
+ */
+ private static final class MarshalPlan {
+ private final MethodHandle handle;
+ private final String failureMessage;
+ private final Throwable failureCause;
+
+ private MarshalPlan(MethodHandle handle, String failureMessage, Throwable failureCause) {
+ this.handle = handle;
+ this.failureMessage = failureMessage;
+ this.failureCause = failureCause;
+ }
+
+ private static MarshalPlan resolve(Class> runtimeType) {
+ try {
+ Method method = runtimeType.getMethod("marshal");
+ if (!PairSeq.class.isAssignableFrom(method.getReturnType())) {
+ return new MarshalPlan(null,
+ "marshal() must return PairSeq in " + runtimeType.getName(), null);
+ }
+ MethodHandle handle = MethodHandles.lookup().unreflect(method);
+ return new MarshalPlan(handle, null, null);
+ } catch (NoSuchMethodException exception) {
+ return new MarshalPlan(null, runtimeType.getName() + " must implement marshal():PairSeq", exception);
+ } catch (IllegalAccessException exception) {
+ return new MarshalPlan(null, "marshal() failed for " + runtimeType.getName(), exception);
+ }
+ }
+
+ @SuppressWarnings("PMD.AvoidCatchingGenericException")
+ private PairSeq invoke(Object value) {
+ if (handle == null) {
+ throw new IllegalStateException(failureMessage, failureCause);
+ }
+ try {
+ return (PairSeq) handle.invoke(value);
+ } catch (Throwable failure) {
+ throw new IllegalStateException("marshal() failed for " + value.getClass().getName(),
+ new InvocationTargetException(failure));
+ }
+ }
+ }
+
+ /**
+ * Cached success or structural failure for one unmarshal runtime class.
+ */
+ private static final class UnmarshalPlan {
+ private final MethodHandle handle;
+ private final String invocationFailureMessage;
+ private final String resolutionFailureMessage;
+ private final Throwable resolutionFailureCause;
+
+ private UnmarshalPlan(MethodHandle handle, String invocationFailureMessage, String resolutionFailureMessage,
+ Throwable resolutionFailureCause) {
+ this.handle = handle;
+ this.invocationFailureMessage = invocationFailureMessage;
+ this.resolutionFailureMessage = resolutionFailureMessage;
+ this.resolutionFailureCause = resolutionFailureCause;
+ }
+
+ private static UnmarshalPlan resolve(Class> runtimeType) {
+ try {
+ Method method = runtimeType.getMethod("unmarshal", PairSeq.class);
+ if (Modifier.isStatic(method.getModifiers())) {
+ Class> returnType = method.getReturnType();
+ if (!runtimeType.isAssignableFrom(returnType) && !returnType.isAssignableFrom(runtimeType)) {
+ return new UnmarshalPlan(null, null,
+ "static unmarshal(PairSeq) must return " + runtimeType.getName(), null);
+ }
+ MethodHandle handle = MethodHandles.lookup().unreflect(method);
+ return new UnmarshalPlan(handle,
+ "static unmarshal(PairSeq) failed for " + runtimeType.getName(), null, null);
+ }
+ } catch (NoSuchMethodException ignored) {
+ // Resolve the constructor fallback below.
+ } catch (IllegalAccessException exception) {
+ return new UnmarshalPlan(null, null,
+ "static unmarshal(PairSeq) failed for " + runtimeType.getName(), exception);
+ }
+
+ try {
+ MethodHandle handle = MethodHandles.lookup()
+ .unreflectConstructor(runtimeType.getConstructor(PairSeq.class));
+ return new UnmarshalPlan(handle, "ctor(PairSeq) failed for " + runtimeType.getName(), null, null);
+ } catch (NoSuchMethodException exception) {
+ return new UnmarshalPlan(null, null,
+ runtimeType.getName() + " must provide static unmarshal(PairSeq) or ctor(PairSeq)", exception);
+ } catch (IllegalAccessException exception) {
+ return new UnmarshalPlan(null, null, "ctor(PairSeq) failed for " + runtimeType.getName(), exception);
+ }
+ }
+
+ @SuppressWarnings("PMD.AvoidCatchingGenericException")
+ private Object invoke(PairSeq representation) {
+ if (handle == null) {
+ throw new IllegalStateException(resolutionFailureMessage, resolutionFailureCause);
+ }
+ try {
+ return handle.invoke(representation);
+ } catch (Throwable failure) {
+ throw new IllegalStateException(invocationFailureMessage, new InvocationTargetException(failure));
+ }
}
}
}
diff --git a/lib/src/main/java/zeroecho/core/policy/CryptoPolicy.java b/lib/src/main/java/zeroecho/core/policy/CryptoPolicy.java
index 8c0f6b9..52a9487 100644
--- a/lib/src/main/java/zeroecho/core/policy/CryptoPolicy.java
+++ b/lib/src/main/java/zeroecho/core/policy/CryptoPolicy.java
@@ -59,16 +59,17 @@ import zeroecho.core.spec.ContextSpec;
*
*
* Typical usage
{@code
- * // Install a global minimum-strength policy
- * CryptoAlgorithms.setPolicy(CryptoPolicy.minStrength(128));
+ * ZeroEchoSession session = new ZeroEchoSession()
+ * .withPolicy(CryptoPolicy.minStrength(128));
*
* // Later, when creating a context:
- * EncryptionContext ctx = CryptoAlgorithms.create("AES/GCM", KeyUsage.ENCRYPT, secretKey);
+ * EncryptionContext ctx = session.createContext("AES", KeyUsage.ENCRYPT, secretKey);
* }
*
* @param context specification type
* @param key type
* @since 1.0
+ * @see zeroecho.sdk.ZeroEchoSession
*/
public interface CryptoPolicy { // NOPMD
/**
diff --git a/lib/src/main/java/zeroecho/core/policy/package-info.java b/lib/src/main/java/zeroecho/core/policy/package-info.java
index ca64411..c589f91 100644
--- a/lib/src/main/java/zeroecho/core/policy/package-info.java
+++ b/lib/src/main/java/zeroecho/core/policy/package-info.java
@@ -52,13 +52,12 @@
*
*
* Typical usage
{@code
- * // Install a global minimum-strength policy.
- * zeroecho.core.CryptoAlgorithms.setPolicy(
- * zeroecho.core.policy.CryptoPolicy.minStrength(128));
+ * zeroecho.sdk.ZeroEchoSession session = new zeroecho.sdk.ZeroEchoSession()
+ * .withPolicy(zeroecho.core.policy.CryptoPolicy.minStrength(128));
*
- * // Later, when creating a context, the policy is consulted automatically.
+ * // The session applies the policy without changing other consumers.
* zeroecho.core.context.EncryptionContext ctx =
- * zeroecho.core.CryptoAlgorithms.create("AES/GCM", zeroecho.core.KeyUsage.ENCRYPT, secretKey);
+ * session.createContext("AES", zeroecho.core.KeyUsage.ENCRYPT, secretKey);
* }
*
* Design notes
diff --git a/lib/src/main/java/zeroecho/core/spec/AlgorithmKeySpec.java b/lib/src/main/java/zeroecho/core/spec/AlgorithmKeySpec.java
index e7d8875..c7eaf76 100644
--- a/lib/src/main/java/zeroecho/core/spec/AlgorithmKeySpec.java
+++ b/lib/src/main/java/zeroecho/core/spec/AlgorithmKeySpec.java
@@ -33,7 +33,6 @@
******************************************************************************/
package zeroecho.core.spec;
-import zeroecho.core.spi.SymmetricKeyBuilder;
/**
* Marker interface for algorithm-specific key specifications.
@@ -46,7 +45,7 @@ import zeroecho.core.spi.SymmetricKeyBuilder;
* Design
*
* - Separates algorithm parameters from key builders such as
- * {@link SymmetricKeyBuilder}.
+ * operation-specific key generator or importer.
* - Provides a type-safe way to pass algorithm requirements around instead of
* raw integers or opaque byte arrays.
* - Allows higher layers to work generically with {@code AlgorithmKeySpec}
diff --git a/lib/src/main/java/zeroecho/core/spec/VoidSpec.java b/lib/src/main/java/zeroecho/core/spec/VoidSpec.java
index aa264c4..256ca51 100644
--- a/lib/src/main/java/zeroecho/core/spec/VoidSpec.java
+++ b/lib/src/main/java/zeroecho/core/spec/VoidSpec.java
@@ -43,7 +43,7 @@ package zeroecho.core.spec;
*
*
* Usage
{@code
- * CryptoContext ctx = algo.create(KeyUsage.SIGN, privateKey, VoidSpec.INSTANCE);
+ * CryptoContext ctx = algo.createContext(KeyUsage.SIGN, privateKey, VoidSpec.INSTANCE);
* }
*
*
diff --git a/lib/src/main/java/zeroecho/core/spec/package-info.java b/lib/src/main/java/zeroecho/core/spec/package-info.java
index 7dd5f1a..4f780f1 100644
--- a/lib/src/main/java/zeroecho/core/spec/package-info.java
+++ b/lib/src/main/java/zeroecho/core/spec/package-info.java
@@ -56,12 +56,12 @@
*
Typical usage
{@code
* // Algorithm requires no per-operation parameters.
* zeroecho.core.context.CryptoContext ctx =
- * algo.create(zeroecho.core.KeyUsage.SIGN, privateKey, zeroecho.core.spec.VoidSpec.INSTANCE);
+ * algo.createContext(zeroecho.core.KeyUsage.SIGN, privateKey, zeroecho.core.spec.VoidSpec.INSTANCE);
*
* // Algorithm with parameters: pass an algorithm-specific ContextSpec implementation.
* // Example: RSA with OAEP/PSS, AEAD tag length, etc.
* // zeroecho.core.context.CryptoContext ctx =
- * // algo.create(zeroecho.core.KeyUsage.ENCRYPT, key, someAlgorithmSpecificSpec);
+ * // algo.createContext(zeroecho.core.KeyUsage.ENCRYPT, key, someAlgorithmSpecificSpec);
* }
*
* Design notes
diff --git a/lib/src/main/java/zeroecho/core/spi/AsymmetricKeyBuilder.java b/lib/src/main/java/zeroecho/core/spi/AsymmetricKeyBuilder.java
deleted file mode 100644
index f3a660a..0000000
--- a/lib/src/main/java/zeroecho/core/spi/AsymmetricKeyBuilder.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2026, Leo Galambos
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * 3. All advertising materials mentioning features or use of this software must
- * display the following acknowledgement:
- * This product includes software developed by the Egothor project.
- *
- * 4. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- ******************************************************************************/
-package zeroecho.core.spi;
-
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-
-import zeroecho.core.spec.AlgorithmKeySpec;
-
-/**
- * Factory interface for constructing asymmetric key pairs and importing
- * public/private keys from specifications.
- *
- *
- * Implementations encapsulate algorithm-specific details (for example RSA,
- * Ed25519, X25519) while exposing a uniform API for generation and import
- * operations. This allows higher-level code to work generically with
- * {@code AsymmetricKeyBuilder} without depending on algorithm internals.
- *
- *
- * Operations
- *
- * - {@link #generateKeyPair(AlgorithmKeySpec)} - creates a fresh key pair
- * using the supplied algorithm spec (such as curve parameters or modulus
- * length).
- * - {@link #importPublic(AlgorithmKeySpec)} - wraps externally supplied
- * public key material in a {@link PublicKey}, validating that it conforms to
- * the algorithm specification.
- * - {@link #importPrivate(AlgorithmKeySpec)} - wraps externally supplied
- * private key material in a {@link PrivateKey}, validating that it conforms to
- * the algorithm specification.
- *
- *
- * Usage guidelines
- *
- * - Always prefer {@link #generateKeyPair(AlgorithmKeySpec)} when creating
- * new credentials.
- * - Use {@link #importPublic(AlgorithmKeySpec)} and
- * {@link #importPrivate(AlgorithmKeySpec)} for interoperability, loading from
- * key stores, or migration from existing material.
- * - Implementations should reject malformed or weak keys and enforce
- * algorithm-specific constraints (for example, minimum modulus length for RSA
- * or disallowed small subgroup curves).
- *
- *
- * Thread safety
Implementations must be stateless or otherwise safe
- * for concurrent use across threads.
- *
- * @param algorithm-specific key specification type
- *
- * @since 1.0
- */
-public interface AsymmetricKeyBuilder {
- /**
- * Generates a new asymmetric key pair according to the given specification.
- *
- * @param spec algorithm parameters, such as modulus length or curve identifier
- * @return a new {@link KeyPair} containing a public and private key
- * @throws GeneralSecurityException if the algorithm or parameters are invalid
- * or unsupported
- */
- KeyPair generateKeyPair(S spec) throws GeneralSecurityException;
-
- /**
- * Imports an externally supplied public key according to the given
- * specification.
- *
- *
- * Implementations must validate that the provided material is properly
- * formatted, has acceptable length, and is consistent with the specified
- * algorithm.
- *
- *
- * @param spec algorithm parameters and encoded public key material
- * @return a {@link PublicKey} validated and usable for cryptographic operations
- * @throws GeneralSecurityException if the key material is invalid or does not
- * match the specification
- */
- PublicKey importPublic(S spec) throws GeneralSecurityException;
-
- /**
- * Imports an externally supplied private key according to the given
- * specification.
- *
- *
- * Implementations must validate that the provided material is properly
- * formatted, has acceptable length, and is consistent with the specified
- * algorithm.
- *
- *
- * @param spec algorithm parameters and encoded private key material
- * @return a {@link PrivateKey} validated and usable for cryptographic
- * operations
- * @throws GeneralSecurityException if the key material is invalid or does not
- * match the specification
- */
- PrivateKey importPrivate(S spec) throws GeneralSecurityException;
-}
diff --git a/lib/src/main/java/zeroecho/core/spi/AsymmetricKeyPairGenerator.java b/lib/src/main/java/zeroecho/core/spi/AsymmetricKeyPairGenerator.java
new file mode 100644
index 0000000..804d81d
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/spi/AsymmetricKeyPairGenerator.java
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ ******************************************************************************/
+package zeroecho.core.spi;
+
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+
+import zeroecho.core.spec.AlgorithmKeySpec;
+
+/**
+ * Generates asymmetric key pairs for one exact specification type.
+ * Implementations must be stateless or otherwise safe for concurrent invocation.
+ *
+ * @param specification type
+ * @since 1.0
+ */
+@FunctionalInterface
+public interface AsymmetricKeyPairGenerator {
+ /**
+ * Generates a key pair.
+ *
+ * @param spec generation parameters
+ * @return generated key pair
+ * @throws GeneralSecurityException if generation fails
+ */
+ KeyPair generateKeyPair(S spec) throws GeneralSecurityException;
+}
diff --git a/lib/src/main/java/zeroecho/core/spi/ContextConstructorKS.java b/lib/src/main/java/zeroecho/core/spi/ContextConstructorKS.java
deleted file mode 100644
index e5854b3..0000000
--- a/lib/src/main/java/zeroecho/core/spi/ContextConstructorKS.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2026, Leo Galambos
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * 3. All advertising materials mentioning features or use of this software must
- * display the following acknowledgement:
- * This product includes software developed by the Egothor project.
- *
- * 4. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- ******************************************************************************/
-package zeroecho.core.spi;
-
-import java.io.IOException;
-import java.security.Key;
-
-import zeroecho.core.CryptoAlgorithm;
-import zeroecho.core.context.CryptoContext;
-import zeroecho.core.spec.ContextSpec;
-
-/**
- * Factory interface to construct a {@link CryptoContext} from a key and an
- * optional specification.
- *
- *
- * Each cryptographic algorithm binds one or more roles (for example,
- * {@code ENCRYPT}, {@code SIGN}) to a corresponding context type. For each
- * binding, a {@code ContextConstructorKS} is registered as the factory that
- * creates the runtime context for the role. The role itself is implied by the
- * registration and does not need to be passed explicitly here.
- *
- *
- * Responsibilities
- *
- * - Validate that the provided key is compatible with the expected key
- * type.
- * - Interpret the {@link ContextSpec} parameters (such as IVs, padding modes,
- * or curve identifiers).
- * - Construct and return a ready-to-use {@link CryptoContext} instance bound
- * to the given key and spec.
- *
- *
- * Usage
- *
- * {@code ContextConstructorKS} is primarily used internally by
- * {@link CryptoAlgorithm} implementations when binding roles. Higher-level code
- * should not call it directly; instead use {@link CryptoAlgorithm#create} or
- * {@link zeroecho.core.CryptoAlgorithms#create}.
- *
- *
- * Thread safety
Implementations should be stateless and safe to invoke
- * concurrently from multiple threads.
- *
- * @param context type produced
- * @param key type accepted
- * @param specification type accepted
- *
- * @since 1.0
- */
-@FunctionalInterface
-public interface ContextConstructorKS {
- /**
- * Creates a new {@link CryptoContext} instance bound to the provided key and
- * specification.
- *
- * @param key non-null cryptographic key suitable for the role
- * @param spec role-specific parameters; may be {@code null} if defaults are
- * acceptable
- * @return a newly constructed context ready for cryptographic operations
- * @throws IOException if context creation fails due to I/O, provider issues, or
- * invalid parameters
- */
- C create(K key, S spec) throws IOException;
-}
diff --git a/lib/src/main/java/zeroecho/core/spi/ContextFactoryKS.java b/lib/src/main/java/zeroecho/core/spi/ContextFactoryKS.java
new file mode 100644
index 0000000..2af71e3
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/spi/ContextFactoryKS.java
@@ -0,0 +1,39 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the conditions in the project LICENSE are met.
+ ******************************************************************************/
+package zeroecho.core.spi;
+
+import java.security.Key;
+
+import zeroecho.core.context.CryptoContext;
+import zeroecho.core.spec.ContextSpec;
+
+/**
+ * Creates a cryptographic context from a key and a context specification.
+ *
+ * Implementations report provider and parameter failures with unchecked
+ * exceptions; context construction is a pure in-memory operation and does not
+ * expose an I/O failure contract. Factories must be stateless or otherwise safe
+ * for concurrent invocation; returned contexts retain their own documented
+ * thread-safety contracts.
+ *
+ * @param context type produced
+ * @param key type accepted
+ * @param specification type accepted
+ * @since 1.0
+ */
+@FunctionalInterface
+public interface ContextFactoryKS {
+ /**
+ * Creates a context bound to the supplied key and specification.
+ *
+ * @param key non-null key
+ * @param spec non-null resolved context specification
+ * @return a newly created context
+ */
+ C createContext(K key, S spec);
+}
diff --git a/lib/src/main/java/zeroecho/core/spi/PrivateKeyImporter.java b/lib/src/main/java/zeroecho/core/spi/PrivateKeyImporter.java
new file mode 100644
index 0000000..6cb7c44
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/spi/PrivateKeyImporter.java
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ ******************************************************************************/
+package zeroecho.core.spi;
+
+import java.security.GeneralSecurityException;
+import java.security.PrivateKey;
+
+import zeroecho.core.spec.AlgorithmKeySpec;
+
+/**
+ * Imports private keys for one exact specification type. Implementations must be
+ * stateless or otherwise safe for concurrent invocation.
+ *
+ * @param specification type
+ * @since 1.0
+ */
+@FunctionalInterface
+public interface PrivateKeyImporter {
+ /**
+ * Imports a private key.
+ *
+ * @param spec encoded key material and parameters
+ * @return imported private key
+ * @throws GeneralSecurityException if validation or import fails
+ */
+ PrivateKey importPrivate(S spec) throws GeneralSecurityException;
+}
diff --git a/lib/src/main/java/zeroecho/core/spi/PublicKeyImporter.java b/lib/src/main/java/zeroecho/core/spi/PublicKeyImporter.java
new file mode 100644
index 0000000..3c90dbb
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/spi/PublicKeyImporter.java
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ ******************************************************************************/
+package zeroecho.core.spi;
+
+import java.security.GeneralSecurityException;
+import java.security.PublicKey;
+
+import zeroecho.core.spec.AlgorithmKeySpec;
+
+/**
+ * Imports public keys for one exact specification type. Implementations must be
+ * stateless or otherwise safe for concurrent invocation.
+ *
+ * @param specification type
+ * @since 1.0
+ */
+@FunctionalInterface
+public interface PublicKeyImporter {
+ /**
+ * Imports a public key.
+ *
+ * @param spec encoded key material and parameters
+ * @return imported public key
+ * @throws GeneralSecurityException if validation or import fails
+ */
+ PublicKey importPublic(S spec) throws GeneralSecurityException;
+}
diff --git a/lib/src/main/java/zeroecho/core/spi/SymmetricKeyBuilder.java b/lib/src/main/java/zeroecho/core/spi/SymmetricKeyBuilder.java
deleted file mode 100644
index 53c91dd..0000000
--- a/lib/src/main/java/zeroecho/core/spi/SymmetricKeyBuilder.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2026, Leo Galambos
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * 3. All advertising materials mentioning features or use of this software must
- * display the following acknowledgement:
- * This product includes software developed by the Egothor project.
- *
- * 4. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- ******************************************************************************/
-package zeroecho.core.spi;
-
-import java.security.GeneralSecurityException;
-
-import javax.crypto.SecretKey;
-
-import zeroecho.core.spec.AlgorithmKeySpec;
-
-/**
- * Factory interface for constructing symmetric keys from algorithm-specific
- * specifications.
- *
- *
- * Implementations encapsulate the details of generating or importing
- * {@link SecretKey} instances for a particular symmetric algorithm (for example
- * AES, ChaCha20, or HMAC). This abstraction provides a uniform API for higher
- * layers, independent of provider-specific implementations.
- *
- *
- * Operations
- *
- * - {@link #generateSecret(AlgorithmKeySpec)} - creates a fresh random key
- * using the parameters supplied by the algorithm specification.
- * - {@link #importSecret(AlgorithmKeySpec)} - wraps externally supplied raw
- * key material in a {@link SecretKey}, validating that it conforms to the
- * specification.
- *
- *
- * Usage guidelines
- *
- * - Prefer {@link #generateSecret(AlgorithmKeySpec)} for new credentials to
- * ensure strong, random keys.
- * - Use {@link #importSecret(AlgorithmKeySpec)} only when loading existing
- * keys, migrating from another system, or interoperating with external storage
- * formats.
- * - Implementations must enforce algorithm constraints, including required
- * key sizes and disallowing known-weak parameters.
- * - Returned {@link SecretKey} instances should be immutable and, where
- * possible, wrapped in provider-specific classes that prevent serialization or
- * unintended exposure.
- *
- *
- * Thread safety
- *
- * Implementations must be stateless or otherwise safe to use concurrently
- * across multiple threads.
- *
- *
- * @param algorithm-specific key specification type
- *
- * @since 1.0
- */
-public interface SymmetricKeyBuilder {
- /**
- * Generates a new symmetric key according to the given specification.
- *
- * @param spec algorithm parameters, such as required key size or algorithm
- * variant
- * @return a freshly generated {@link SecretKey} containing random key material
- * @throws GeneralSecurityException if key generation fails or the parameters
- * are invalid or unsupported
- */
- SecretKey generateSecret(S spec) throws GeneralSecurityException;
-
- /**
- * Imports an externally provided symmetric key according to the given
- * specification.
- *
- *
- * Implementations must validate that the provided material matches the
- * algorithm’s requirements (for example, correct length and encoding). Weak or
- * truncated keys must be rejected.
- *
- *
- * @param spec algorithm parameters and raw key material
- * @return a validated {@link SecretKey} suitable for cryptographic use
- * @throws GeneralSecurityException if the key material is invalid, corrupted,
- * or inconsistent with the specification
- */
- SecretKey importSecret(S spec) throws GeneralSecurityException;
-}
diff --git a/lib/src/main/java/zeroecho/core/spi/SymmetricKeyGenerator.java b/lib/src/main/java/zeroecho/core/spi/SymmetricKeyGenerator.java
new file mode 100644
index 0000000..2340324
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/spi/SymmetricKeyGenerator.java
@@ -0,0 +1,30 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ ******************************************************************************/
+package zeroecho.core.spi;
+
+import java.security.GeneralSecurityException;
+
+import javax.crypto.SecretKey;
+
+import zeroecho.core.spec.AlgorithmKeySpec;
+
+/**
+ * Generates symmetric keys for one exact specification type. Implementations
+ * must be stateless or otherwise safe for concurrent invocation.
+ *
+ * @param specification type
+ * @since 1.0
+ */
+@FunctionalInterface
+public interface SymmetricKeyGenerator {
+ /**
+ * Generates a symmetric key.
+ *
+ * @param spec generation parameters
+ * @return generated key
+ * @throws GeneralSecurityException if generation fails
+ */
+ SecretKey generateSecret(S spec) throws GeneralSecurityException;
+}
diff --git a/lib/src/main/java/zeroecho/core/spi/SymmetricKeyImporter.java b/lib/src/main/java/zeroecho/core/spi/SymmetricKeyImporter.java
new file mode 100644
index 0000000..d7ebb32
--- /dev/null
+++ b/lib/src/main/java/zeroecho/core/spi/SymmetricKeyImporter.java
@@ -0,0 +1,30 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ ******************************************************************************/
+package zeroecho.core.spi;
+
+import java.security.GeneralSecurityException;
+
+import javax.crypto.SecretKey;
+
+import zeroecho.core.spec.AlgorithmKeySpec;
+
+/**
+ * Imports symmetric keys for one exact specification type. Implementations must
+ * be stateless or otherwise safe for concurrent invocation.
+ *
+ * @param specification type
+ * @since 1.0
+ */
+@FunctionalInterface
+public interface SymmetricKeyImporter {
+ /**
+ * Imports a symmetric key.
+ *
+ * @param spec encoded key material and parameters
+ * @return imported key
+ * @throws GeneralSecurityException if validation or import fails
+ */
+ SecretKey importSecret(S spec) throws GeneralSecurityException;
+}
diff --git a/lib/src/main/java/zeroecho/core/spi/package-info.java b/lib/src/main/java/zeroecho/core/spi/package-info.java
index fc23fb1..896d4bf 100644
--- a/lib/src/main/java/zeroecho/core/spi/package-info.java
+++ b/lib/src/main/java/zeroecho/core/spi/package-info.java
@@ -32,152 +32,22 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
/**
- * Service Provider Interfaces (SPI) for extending ZeroEcho with custom
- * cryptographic algorithms and key builders.
+ * Provider contracts for context construction and exact key operations.
*
- *
- * This package defines the provider-facing contracts used by algorithms to plug
- * new primitives into the framework while keeping the public API uniform. The
- * SPIs emphasize role-driven context construction, strict spec validation, and
- * safe key lifecycle handling.
- *
+ * Algorithms bind each supported role to a {@link ContextFactoryKS}. Context
+ * construction is an in-memory operation; stream attachment and processing are
+ * responsible for reporting {@link java.io.IOException}.
*
- * How algorithms plug in
- *
- * An algorithm publishes capabilities and binds each supported
- * {@link zeroecho.core.KeyUsage role} to a factory that creates a matching
- * {@link zeroecho.core.context.CryptoContext}. The binding is registered in the
- * algorithm constructor using {@link ContextConstructorKS}; the role is implied
- * by the binding itself.
- *
+ * Key capabilities are registered independently through
+ * {@link SymmetricKeyGenerator}, {@link SymmetricKeyImporter},
+ * {@link AsymmetricKeyPairGenerator}, {@link PublicKeyImporter}, and
+ * {@link PrivateKeyImporter}. A provider registers only the operations it
+ * implements, so capability lookup fails before invocation instead of returning
+ * an object with unsupported methods.
*
- * {@code
- * // Inside an algorithm's constructor (illustrative):
- * capability(AlgorithmFamily.SYMMETRIC, KeyUsage.ENCRYPT,
- * zeroecho.core.context.EncryptionContext.class,
- * javax.crypto.SecretKey.class, zeroecho.core.alg.aes.AesSpec.class,
- * (k, s) -> new zeroecho.core.alg.aes.AesCipherContext(this, k, true, s, new java.security.SecureRandom()),
- * () -> zeroecho.core.alg.aes.AesSpec.gcm128(null));
- *
- * capability(AlgorithmFamily.SYMMETRIC, KeyUsage.DECRYPT,
- * zeroecho.core.context.EncryptionContext.class,
- * javax.crypto.SecretKey.class, zeroecho.core.alg.aes.AesSpec.class,
- * (k, s) -> new zeroecho.core.alg.aes.AesCipherContext(this, k, false, s, new java.security.SecureRandom()),
- * () -> zeroecho.core.alg.aes.AesSpec.gcm128(null));
- * }
- *
- * Key material builders and the spec-class keyed registry
- *
- * Builders are registered per spec class. The algorithm maintains a single map
- * from {@code Class extends zeroecho.core.spec.AlgorithmKeySpec>} to a
- * builder instance; lookups are driven by the spec class, not by the high-level
- * operation. This keeps registration simple while letting providers model
- * different intents through different spec types.
- *
- *
- *
- * - {@link AsymmetricKeyBuilder} provides key pair generation and
- * public/private key import for asymmetric algorithms.
- * - {@link SymmetricKeyBuilder} provides key generation and key import for
- * {@link javax.crypto.SecretKey}-based algorithms.
- *
- *
- * Why a single interface for both key pair generation and key import
- *
- * There is one cohesive builder interface because the registry keys off the
- * spec class, not the operation. A single builder type per spec class gives one
- * lookup path and one place to enforce spec parsing, format checks, curve or
- * modulus validation, and provider constraints. The public facades in
- * {@link zeroecho.core.CryptoAlgorithm} and
- * {@link zeroecho.core.CryptoAlgorithms} remain compact: they resolve the
- * builder by spec class and then invoke
- * {@link AsymmetricKeyBuilder#generateKeyPair(zeroecho.core.spec.AlgorithmKeySpec)},
- * {@link AsymmetricKeyBuilder#importPublic(zeroecho.core.spec.AlgorithmKeySpec)},
- * {@link AsymmetricKeyBuilder#importPrivate(zeroecho.core.spec.AlgorithmKeySpec)},
- * {@link SymmetricKeyBuilder#generateSecret(zeroecho.core.spec.AlgorithmKeySpec)},
- * or
- * {@link SymmetricKeyBuilder#importSecret(zeroecho.core.spec.AlgorithmKeySpec)}
- * as appropriate.
- *
- *
- * Providers express differences in capability by choosing distinct spec classes
- * rather than multiplying interfaces. For example, a symmetric provider may
- * register {@code AesKeyGenSpec} for generation and {@code AesKeyImportSpec}
- * for import, each with its own builder. The unified interface still fits
- * because the registry discriminates by spec class.
- *
- *
- * Pattern: separate specs for generation vs import with prescriptive
- * exceptions
- *
- * It is idiomatic to register two builders for AES: one bound to a generation
- * spec that implements
- * {@link SymmetricKeyBuilder#generateSecret(zeroecho.core.spec.AlgorithmKeySpec)}
- * and rejects import, and one bound to an import spec that implements
- * {@link SymmetricKeyBuilder#importSecret(zeroecho.core.spec.AlgorithmKeySpec)}
- * and rejects generation. Unsupported paths should throw
- * {@link UnsupportedOperationException} with a clear, prescriptive message that
- * points to the correct spec.
- *
- *
- * {@code
- * // Generation-only builder bound to AesKeyGenSpec
- * registerSymmetricKeyBuilder(zeroecho.core.alg.aes.AesKeyGenSpec.class, new SymmetricKeyBuilder<>() {
- * @Override public javax.crypto.SecretKey generateSecret(zeroecho.core.alg.aes.AesKeyGenSpec spec)
- * throws java.security.GeneralSecurityException {
- * // generate according to spec.keySizeBits()
- * throw new UnsupportedOperationException("example");
- * }
- * @Override public javax.crypto.SecretKey importSecret(zeroecho.core.alg.aes.AesKeyGenSpec spec) {
- * throw new UnsupportedOperationException("Use AesKeyImportSpec for importing AES keys");
- * }
- * }, zeroecho.core.alg.aes.AesKeyGenSpec::aes256);
- *
- * // Import-only builder bound to AesKeyImportSpec
- * registerSymmetricKeyBuilder(zeroecho.core.alg.aes.AesKeyImportSpec.class, new SymmetricKeyBuilder<>() {
- * @Override public javax.crypto.SecretKey generateSecret(zeroecho.core.alg.aes.AesKeyImportSpec spec) {
- * throw new UnsupportedOperationException("Use AesKeyGenSpec to generate AES keys");
- * }
- * @Override public javax.crypto.SecretKey importSecret(zeroecho.core.alg.aes.AesKeyImportSpec spec) {
- * return new javax.crypto.spec.SecretKeySpec(spec.key(), "AES");
- * }
- * }, null);
- * }
- *
- *
- * The same separation can be applied to asymmetric algorithms by using, for
- * example, {@code RsaKeyGenSpec} and {@code RsaKeyImportSpec}. Each spec class
- * maps to exactly one {@link AsymmetricKeyBuilder} in the algorithm's registry.
- *
- *
- * Context sharing and parameter flow
- *
- * Contexts that need per-session values (IV, nonce, salt, AAD) may implement
- * {@link ContextAware} to read and write through a shared
- * {@link conflux.CtxInterface}. For symmetric streams that carry lightweight
- * headers, algorithms in {@code zeroecho.core} can use
- * {@link zeroecho.core.SymmetricHeaderCodec}.
- *
- *
- * Error handling and validation
- *
- * - Fail fast when keys or specs are incompatible with the bound role.
- * - Use {@link java.security.GeneralSecurityException} for key generation or
- * import failures.
- * - Use {@link java.io.IOException} from
- * {@link ContextConstructorKS#create(java.security.Key, zeroecho.core.spec.ContextSpec)}
- * when context setup performs I/O.
- * - Use {@link UnsupportedOperationException} with a precise message when an
- * operation is intentionally unsupported by the spec or provider, for example
- * "Use AesKeyImportSpec for importing AES keys".
- *
- *
- * Thread safety
- *
- * SPI implementations should be stateless or otherwise safe for concurrent use.
- * Created contexts are not necessarily thread-safe unless explicitly documented
- * by the provider.
- *
+ * SPI implementations should be stateless or otherwise safe for concurrent
+ * lookup and invocation. Created cryptographic contexts remain operation-local
+ * and are not necessarily thread-safe.
*
* @since 1.0
*/
diff --git a/lib/src/main/java/zeroecho/core/storage/KeyringStore.java b/lib/src/main/java/zeroecho/core/storage/KeyringStore.java
index 6b91b88..cadd102 100644
--- a/lib/src/main/java/zeroecho/core/storage/KeyringStore.java
+++ b/lib/src/main/java/zeroecho/core/storage/KeyringStore.java
@@ -52,12 +52,18 @@ import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import javax.crypto.SecretKey;
+import javax.security.auth.DestroyFailedException;
+import javax.security.auth.Destroyable;
-import zeroecho.core.CryptoAlgorithms;
+import zeroecho.core.CryptoAlgorithm;
+import zeroecho.core.KeyOperation;
+import zeroecho.core.KeyOperationInfo;
import zeroecho.core.marshal.PairSeq;
import zeroecho.core.spec.AlgorithmKeySpec;
+import zeroecho.sdk.ZeroEchoSession;
/**
* Human-editable keyring persisted in a simple UTF-8 text format.
@@ -82,7 +88,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* }
*
* Reading and writing
Use {@link #save(java.nio.file.Path)} to write
- * the keyring to disk and {@link #load(java.nio.file.Path)} to read it back.
+ * the keyring to disk and {@link #load(ZeroEchoSession, java.nio.file.Path)} to
+ * read it back.
* The loader tolerates the presence of the header and comment lines but
* requires the magic header for the v1 format.
*
@@ -95,15 +102,16 @@ import zeroecho.core.spec.AlgorithmKeySpec;
*
* These are discovered and invoked via reflection. See
* {@link #marshalSpec(AlgorithmKeySpec)} and
- * {@link #unmarshalSpec(String, PairSeq)} for details.
+ * {@link #unmarshalSpec(Class, PairSeq)} for details.
*
* Basic usage
{@code
- * KeyringStore ks = new KeyringStore();
+ * ZeroEchoSession session = new ZeroEchoSession();
+ * KeyringStore ks = new KeyringStore(session);
* ks.putPublic("site-signing", "Ed25519", myEd25519PublicSpec);
* ks.putPrivate("site-signing", "Ed25519", myEd25519PrivateSpec);
* ks.save(Path.of("keyring.txt"));
*
- * KeyringStore reloaded = KeyringStore.load(Path.of("keyring.txt"));
+ * KeyringStore reloaded = KeyringStore.load(session, Path.of("keyring.txt"));
* PublicKey pub = reloaded.getPublic("site-signing");
* }
*/
@@ -120,6 +128,17 @@ public final class KeyringStore { // NOPMD
private static final String SUFFIX_PRIVATE = ".priv";
private final Map byAlias = new LinkedHashMap<>();
+ private final ZeroEchoSession session;
+
+ /**
+ * Creates an empty keyring bound to a runtime session.
+ *
+ * @param session explicit runtime configuration
+ * @throws NullPointerException if {@code session} is {@code null}
+ */
+ public KeyringStore(ZeroEchoSession session) {
+ this.session = Objects.requireNonNull(session, "session must not be null");
+ }
/**
* Immutable entry in a {@link KeyringStore}.
@@ -228,7 +247,7 @@ public final class KeyringStore { // NOPMD
* PublicWithId pairs the algorithm identifier with a resolved public key.
*
* Usage
{@code
- * KeyringStore ks = KeyringStore.load(path);
+ * KeyringStore ks = KeyringStore.load(session, path);
* KeyringStore.PublicWithId r = ks.getPublicWithId("alice");
* String algId = r.algorithm();
* PublicKey pub = r.key();
@@ -241,7 +260,7 @@ public final class KeyringStore { // NOPMD
* PrivateWithId pairs the algorithm identifier with a resolved private key.
*
* Usage
{@code
- * KeyringStore ks = KeyringStore.load(path);
+ * KeyringStore ks = KeyringStore.load(session, path);
* KeyringStore.PrivateWithId r = ks.getPrivateWithId("alice");
* String algId = r.algorithm();
* PrivateKey prv = r.key();
@@ -254,7 +273,7 @@ public final class KeyringStore { // NOPMD
* SecretWithId pairs the algorithm identifier with a resolved secret key.
*
* Usage
{@code
- * KeyringStore ks = KeyringStore.load(path);
+ * KeyringStore ks = KeyringStore.load(session, path);
* KeyringStore.SecretWithId r = ks.getSecretWithId("hmac-key");
* String algId = r.algorithm();
* SecretKey sk = r.key();
@@ -280,8 +299,8 @@ public final class KeyringStore { // NOPMD
*/
public PublicWithId getPublicWithId(String alias) throws GeneralSecurityException {
Record r = require(withPublicSuffix(alias), Record.Kind.PUBLIC_KEY);
- AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload);
- PublicKey key = CryptoAlgorithms.publicKey(r.algorithm, spec);
+ AlgorithmKeySpec spec = unmarshalRecord(r);
+ PublicKey key = session.keyBuilders().asymmetric().importPublic(r.algorithm, spec);
return new PublicWithId(r.algorithm, key);
}
@@ -296,9 +315,17 @@ public final class KeyringStore { // NOPMD
*/
public PrivateWithId getPrivateWithId(String alias) throws GeneralSecurityException {
Record r = require(withPrivateSuffix(alias), Record.Kind.PRIVATE_KEY);
- AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload);
- PrivateKey key = CryptoAlgorithms.privateKey(r.algorithm, spec);
- return new PrivateWithId(r.algorithm, key);
+ AlgorithmKeySpec spec = unmarshalRecord(r);
+ Throwable failure = null;
+ try {
+ PrivateKey key = session.keyBuilders().asymmetric().importPrivate(r.algorithm, spec);
+ return new PrivateWithId(r.algorithm, key);
+ } catch (GeneralSecurityException | RuntimeException | Error exception) { // NOPMD - retain primary failure
+ failure = exception;
+ throw exception;
+ } finally {
+ destroyTemporarySpec(spec, failure);
+ }
}
/**
@@ -312,9 +339,17 @@ public final class KeyringStore { // NOPMD
*/
public SecretWithId getSecretWithId(String alias) throws GeneralSecurityException {
Record r = require(alias, Record.Kind.SECRET_KEY);
- AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload);
- SecretKey key = CryptoAlgorithms.secretKey(r.algorithm, spec);
- return new SecretWithId(r.algorithm, key);
+ AlgorithmKeySpec spec = unmarshalRecord(r);
+ Throwable failure = null;
+ try {
+ SecretKey key = session.keyBuilders().symmetric().importKey(r.algorithm, spec);
+ return new SecretWithId(r.algorithm, key);
+ } catch (GeneralSecurityException | RuntimeException | Error exception) { // NOPMD - retain primary failure
+ failure = exception;
+ throw exception;
+ } finally {
+ destroyTemporarySpec(spec, failure);
+ }
}
/**
@@ -322,7 +357,7 @@ public final class KeyringStore { // NOPMD
*
*
* The stored spec class is loaded and unmarshaled via
- * {@link #unmarshalSpec(String, PairSeq)}, and the key is materialized via the
+ * {@link #unmarshalSpec(Class, PairSeq)}, and the key is materialized via the
* crypto catalog.
*
*
@@ -335,8 +370,8 @@ public final class KeyringStore { // NOPMD
*/
public PublicKey getPublic(String alias) throws GeneralSecurityException {
Record r = require(withPublicSuffix(alias), Record.Kind.PUBLIC_KEY);
- AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload);
- return CryptoAlgorithms.publicKey(r.algorithm, spec);
+ AlgorithmKeySpec spec = unmarshalRecord(r);
+ return session.keyBuilders().asymmetric().importPublic(r.algorithm, spec);
}
/**
@@ -351,8 +386,16 @@ public final class KeyringStore { // NOPMD
*/
public PrivateKey getPrivate(String alias) throws GeneralSecurityException {
Record r = require(withPrivateSuffix(alias), Record.Kind.PRIVATE_KEY);
- AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload);
- return CryptoAlgorithms.privateKey(r.algorithm, spec);
+ AlgorithmKeySpec spec = unmarshalRecord(r);
+ Throwable failure = null;
+ try {
+ return session.keyBuilders().asymmetric().importPrivate(r.algorithm, spec);
+ } catch (GeneralSecurityException | RuntimeException | Error exception) { // NOPMD - retain primary failure
+ failure = exception;
+ throw exception;
+ } finally {
+ destroyTemporarySpec(spec, failure);
+ }
}
private static String withPublicSuffix(String baseAlias) {
@@ -401,8 +444,16 @@ public final class KeyringStore { // NOPMD
*/
public SecretKey getSecret(String alias) throws GeneralSecurityException {
Record r = require(alias, Record.Kind.SECRET_KEY);
- AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload);
- return CryptoAlgorithms.secretKey(r.algorithm, spec);
+ AlgorithmKeySpec spec = unmarshalRecord(r);
+ Throwable failure = null;
+ try {
+ return session.keyBuilders().symmetric().importKey(r.algorithm, spec);
+ } catch (GeneralSecurityException | RuntimeException | Error exception) { // NOPMD - retain primary failure
+ failure = exception;
+ throw exception;
+ } finally {
+ destroyTemporarySpec(spec, failure);
+ }
}
/**
@@ -420,14 +471,15 @@ public final class KeyringStore { // NOPMD
/**
* Loads a keyring from a UTF-8 text file.
*
+ * @param session explicit runtime configuration
* @param path source path
* @return a new store populated with entries from the file
* @throws IOException if reading fails or the format is not supported
*/
- public static KeyringStore load(Path path) throws IOException {
+ public static KeyringStore load(ZeroEchoSession session, Path path) throws IOException {
try (BufferedReader r = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
List recs = readAll(r, /* requireHeader */ true);
- KeyringStore store = new KeyringStore();
+ KeyringStore store = new KeyringStore(session);
recs.forEach(rec -> store.byAlias.put(rec.alias, rec));
return store;
}
@@ -616,6 +668,7 @@ public final class KeyringStore { // NOPMD
if (importSpec == null) {
throw new IllegalArgumentException("importSpec");
}
+ registeredSpecClass(algorithmId, kind, importSpec.getClass().getName());
PairSeq payload = marshalSpec(importSpec);
@@ -659,21 +712,69 @@ public final class KeyringStore { // NOPMD
* Calls a static {@code unmarshal(PairSeq)} method on the spec class.
*
* @param spec type
- * @param specClass fully qualified spec class name
+ * @param specClass registered specification class
* @param p the pair sequence to unmarshal
* @return the reconstructed spec instance
* @throws IllegalStateException if reflection fails or the method is absent
*/
@SuppressWarnings("unchecked")
- private static S unmarshalSpec(String specClass, PairSeq p) {
+ private static S unmarshalSpec(Class specClass, PairSeq p) {
try {
- Class> cls = Class.forName(specClass);
- Method m = cls.getMethod("unmarshal", PairSeq.class);
+ Method m = specClass.getMethod("unmarshal", PairSeq.class);
Object out = m.invoke(null, p);
return (S) out;
- } catch (IllegalAccessException | InvocationTargetException | ClassNotFoundException | NoSuchMethodException
- | SecurityException e) {
- throw new IllegalStateException("Spec unmarshal failed for " + specClass, e);
+ } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
+ throw new IllegalStateException("Spec unmarshal failed for " + specClass.getName(), e);
+ }
+ }
+
+ private AlgorithmKeySpec unmarshalRecord(Record record) {
+ Class extends AlgorithmKeySpec> specClass = registeredSpecClass(record.algorithm, record.kind,
+ record.specClass);
+ return unmarshalSpec(specClass, record.specPayload);
+ }
+
+ private Class extends AlgorithmKeySpec> registeredSpecClass(String algorithmId, Record.Kind kind,
+ String persistedClassName) {
+ if (persistedClassName == null || persistedClassName.isBlank()) {
+ throw new IllegalArgumentException("Missing key specification class");
+ }
+ CryptoAlgorithm algorithm = session.require(algorithmId);
+ KeyOperation expectedOperation = switch (kind) {
+ case PUBLIC_KEY -> KeyOperation.ASYMMETRIC_PUBLIC_IMPORT;
+ case PRIVATE_KEY -> KeyOperation.ASYMMETRIC_PRIVATE_IMPORT;
+ case SECRET_KEY -> KeyOperation.SYMMETRIC_IMPORT;
+ };
+ return algorithm.keyOperations().stream()
+ .filter(info -> info.operation() == expectedOperation)
+ .map(KeyOperationInfo::specType)
+ .filter(type -> type.getName().equals(persistedClassName))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException(
+ "Specification class is not registered for " + algorithmId + " " + kind));
+ }
+
+ /* default */ static void destroyTemporarySpec(AlgorithmKeySpec spec, Throwable primary)
+ throws GeneralSecurityException {
+ if (!(spec instanceof Destroyable destroyable)) {
+ return;
+ }
+ try {
+ if (!destroyable.isDestroyed()) {
+ destroyable.destroy();
+ }
+ } catch (DestroyFailedException failure) {
+ if (primary != null) {
+ primary.addSuppressed(failure);
+ return;
+ }
+ throw new GeneralSecurityException("Temporary key specification destruction failed", failure);
+ } catch (RuntimeException failure) { // NOPMD - preserve cleanup failure and primary failure
+ if (primary != null) {
+ primary.addSuppressed(failure);
+ return;
+ }
+ throw failure;
}
}
}
diff --git a/lib/src/main/java/zeroecho/core/storage/package-info.java b/lib/src/main/java/zeroecho/core/storage/package-info.java
index ce7a43a..8797532 100644
--- a/lib/src/main/java/zeroecho/core/storage/package-info.java
+++ b/lib/src/main/java/zeroecho/core/storage/package-info.java
@@ -85,20 +85,21 @@
* static SpecType unmarshal(PairSeq pairs)
*
*
- * See {@link KeyringStore#marshalSpec(zeroecho.core.spec.AlgorithmKeySpec)} and
- * {@link KeyringStore#unmarshalSpec(String, zeroecho.core.marshal.PairSeq)} for
- * details.
+ * {@link KeyringStore} validates each persisted specification type against the
+ * selected algorithm's registered import operation before invoking these
+ * methods.
*
*
* Typical usage
{@code
* // Create and persist a keyring.
- * KeyringStore ks = new KeyringStore();
+ * zeroecho.sdk.ZeroEchoSession session = new zeroecho.sdk.ZeroEchoSession();
+ * KeyringStore ks = new KeyringStore(session);
* ks.putPublic("site-signing", "Ed25519", myEd25519PublicSpec);
* ks.putPrivate("site-signing", "Ed25519", myEd25519PrivateSpec);
* ks.save(java.nio.file.Path.of("keyring.txt"));
*
* // Load and resolve a key later.
- * KeyringStore reloaded = KeyringStore.load(java.nio.file.Path.of("keyring.txt"));
+ * KeyringStore reloaded = KeyringStore.load(session, java.nio.file.Path.of("keyring.txt"));
* java.security.PublicKey pub = reloaded.getPublic("site-signing");
* }
*
diff --git a/lib/src/main/java/zeroecho/core/tag/TagEngineBuilder.java b/lib/src/main/java/zeroecho/core/tag/TagEngineBuilder.java
index 2e3b57c..2aeba64 100644
--- a/lib/src/main/java/zeroecho/core/tag/TagEngineBuilder.java
+++ b/lib/src/main/java/zeroecho/core/tag/TagEngineBuilder.java
@@ -33,7 +33,6 @@
******************************************************************************/
package zeroecho.core.tag;
-import java.io.IOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
@@ -42,7 +41,6 @@ import java.util.function.Supplier;
import javax.crypto.SecretKey;
-import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.NullKey;
import zeroecho.core.alg.digest.DigestSpec;
@@ -50,6 +48,7 @@ import zeroecho.core.alg.ecdsa.EcdsaCurveSpec;
import zeroecho.core.alg.hmac.HmacSpec;
import zeroecho.core.alg.rsa.RsaSigSpec;
import zeroecho.core.spec.ContextSpec;
+import zeroecho.sdk.ZeroEchoSession;
import zeroecho.core.spec.VoidSpec;
/**
@@ -58,8 +57,8 @@ import zeroecho.core.spec.VoidSpec;
*
*
* Each {@code TagEngineBuilder} holds a factory that typically delegates to
- * {@link CryptoAlgorithms#create(String, KeyUsage, java.security.Key, ContextSpec)}
- * so that global policy and auditing are consistently enforced. A new engine
+ * {@link ZeroEchoSession#createContext(String, KeyUsage, java.security.Key, ContextSpec)}
+ * so that session policy and auditing are consistently enforced. A new engine
* instance is created for each call to {@link #get()}.
*
*
@@ -122,16 +121,11 @@ public final class TagEngineBuilder implements Supplier> {
* @param spec digest specification; may be {@code null} to select the default
* @return a builder that produces digest-based {@link TagEngine} instances
*/
- public static TagEngineBuilder digest(final DigestSpec spec) {
+ public static TagEngineBuilder digest(final ZeroEchoSession session, final DigestSpec spec) {
+ Objects.requireNonNull(session, "session");
final DigestSpec s = spec == null ? DigestSpec.sha256() : spec;
- return new TagEngineBuilder<>(() -> {
- try {
- // JcaDigestContext implements DigestContext extends TagEngine
- return CryptoAlgorithms.create("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, s);
- } catch (IOException e) {
- throw new IllegalStateException("Failed to create DIGEST TagEngine", e);
- }
- });
+ return new TagEngineBuilder<>(
+ () -> session.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, s));
}
/**
@@ -146,17 +140,12 @@ public final class TagEngineBuilder implements Supplier> {
* @return a builder that produces HMAC-based {@link TagEngine} instances
* @throws NullPointerException if {@code key} is {@code null}
*/
- public static TagEngineBuilder hmac(final SecretKey key, final HmacSpec spec) {
+ public static TagEngineBuilder hmac(final ZeroEchoSession session, final SecretKey key,
+ final HmacSpec spec) {
+ Objects.requireNonNull(session, "session");
Objects.requireNonNull(key, "key");
final HmacSpec s = spec == null ? HmacSpec.sha256() : spec;
- return new TagEngineBuilder<>(() -> {
- try {
- // HmacMacContext implements MacContext extends TagEngine
- return CryptoAlgorithms.create("HMAC", KeyUsage.MAC, key, s);
- } catch (IOException e) {
- throw new IllegalStateException("Failed to create HMAC TagEngine", e);
- }
- });
+ return new TagEngineBuilder<>(() -> session.createContext("HMAC", KeyUsage.MAC, key, s));
}
/**
@@ -181,8 +170,9 @@ public final class TagEngineBuilder implements Supplier> {
* @throws IllegalArgumentException if {@code key} is not a supported type
* @throws NullPointerException if {@code id} or {@code key} is {@code null}
*/
- public static TagEngineBuilder signature(final String id, final java.security.Key key,
- final ContextSpec spec) {
+ public static TagEngineBuilder signature(final ZeroEchoSession session, final String id,
+ final java.security.Key key, final ContextSpec spec) {
+ Objects.requireNonNull(session, "session");
Objects.requireNonNull(id, "id");
Objects.requireNonNull(key, "key");
@@ -193,15 +183,7 @@ public final class TagEngineBuilder implements Supplier> {
}
final ContextSpec s = spec == null ? VoidSpec.INSTANCE : spec;
- return new TagEngineBuilder<>(() -> {
- try {
- // RsaSignatureContext / Ed25519SignatureContext implement SignatureContext
- // extends TagEngine
- return CryptoAlgorithms.create(id, role, key, s);
- } catch (IOException e) {
- throw new IllegalStateException("Failed to create " + id + " signature TagEngine", e);
- }
- });
+ return new TagEngineBuilder<>(() -> session.createContext(id, role, key, s));
}
/**
@@ -211,9 +193,10 @@ public final class TagEngineBuilder implements Supplier> {
* @return a builder that produces Ed25519 signature engines in SIGN mode
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
- public static TagEngineBuilder ed25519Sign(final PrivateKey privateKey) {
+ public static TagEngineBuilder ed25519Sign(final ZeroEchoSession session,
+ final PrivateKey privateKey) {
Objects.requireNonNull(privateKey, PRIVATE_KEY);
- return signature("Ed25519", privateKey, VoidSpec.INSTANCE);
+ return signature(session, "Ed25519", privateKey, VoidSpec.INSTANCE);
}
/**
@@ -223,9 +206,10 @@ public final class TagEngineBuilder implements Supplier> {
* @return a builder that produces Ed25519 signature engines in VERIFY mode
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
- public static TagEngineBuilder ed25519Verify(final PublicKey publicKey) {
+ public static TagEngineBuilder ed25519Verify(final ZeroEchoSession session,
+ final PublicKey publicKey) {
Objects.requireNonNull(publicKey, PUBLIC_KEY);
- return signature("Ed25519", publicKey, VoidSpec.INSTANCE);
+ return signature(session, "Ed25519", publicKey, VoidSpec.INSTANCE);
}
/**
@@ -242,9 +226,11 @@ public final class TagEngineBuilder implements Supplier> {
* @return a builder that produces RSA signature engines in SIGN mode
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
- public static TagEngineBuilder rsaSign(final PrivateKey privateKey, final RsaSigSpec spec) {
+ public static TagEngineBuilder rsaSign(final ZeroEchoSession session, final PrivateKey privateKey,
+ final RsaSigSpec spec) {
Objects.requireNonNull(privateKey, PRIVATE_KEY);
- return signature("RSA", privateKey, spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
+ return signature(session, "RSA", privateKey,
+ spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
}
/**
@@ -261,9 +247,11 @@ public final class TagEngineBuilder implements Supplier> {
* @return a builder that produces RSA signature engines in VERIFY mode
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
- public static TagEngineBuilder rsaVerify(final PublicKey publicKey, final RsaSigSpec spec) {
+ public static TagEngineBuilder rsaVerify(final ZeroEchoSession session, final PublicKey publicKey,
+ final RsaSigSpec spec) {
Objects.requireNonNull(publicKey, PUBLIC_KEY);
- return signature("RSA", publicKey, spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
+ return signature(session, "RSA", publicKey,
+ spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
}
/**
@@ -279,10 +267,11 @@ public final class TagEngineBuilder implements Supplier> {
* @return a builder that produces ECDSA signature engines in SIGN mode
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
- public static TagEngineBuilder ecdsaSign(final PrivateKey privateKey, final EcdsaCurveSpec spec) {
+ public static TagEngineBuilder ecdsaSign(final ZeroEchoSession session, final PrivateKey privateKey,
+ final EcdsaCurveSpec spec) {
Objects.requireNonNull(privateKey, PRIVATE_KEY);
final EcdsaCurveSpec s = spec == null ? EcdsaCurveSpec.P256 : spec;
- return signature("ECDSA", privateKey, s);
+ return signature(session, "ECDSA", privateKey, s);
}
/**
@@ -298,10 +287,11 @@ public final class TagEngineBuilder implements Supplier> {
* @return a builder that produces ECDSA signature engines in VERIFY mode
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
- public static TagEngineBuilder ecdsaVerify(final PublicKey publicKey, final EcdsaCurveSpec spec) {
+ public static TagEngineBuilder ecdsaVerify(final ZeroEchoSession session, final PublicKey publicKey,
+ final EcdsaCurveSpec spec) {
Objects.requireNonNull(publicKey, PUBLIC_KEY);
final EcdsaCurveSpec s = spec == null ? EcdsaCurveSpec.P256 : spec;
- return signature("ECDSA", publicKey, s);
+ return signature(session, "ECDSA", publicKey, s);
}
/**
@@ -311,9 +301,10 @@ public final class TagEngineBuilder implements Supplier> {
* @return a builder that produces ECDSA signature engines in SIGN mode
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
- public static TagEngineBuilder ecdsaP256Sign(final PrivateKey privateKey) {
+ public static TagEngineBuilder ecdsaP256Sign(final ZeroEchoSession session,
+ final PrivateKey privateKey) {
Objects.requireNonNull(privateKey, PRIVATE_KEY);
- return signature("ECDSA", privateKey, EcdsaCurveSpec.P256);
+ return signature(session, "ECDSA", privateKey, EcdsaCurveSpec.P256);
}
/**
@@ -323,9 +314,10 @@ public final class TagEngineBuilder implements Supplier> {
* @return a builder that produces ECDSA signature engines in VERIFY mode
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
- public static TagEngineBuilder ecdsaP256Verify(final PublicKey publicKey) {
+ public static TagEngineBuilder ecdsaP256Verify(final ZeroEchoSession session,
+ final PublicKey publicKey) {
Objects.requireNonNull(publicKey, PUBLIC_KEY);
- return signature("ECDSA", publicKey, EcdsaCurveSpec.P256);
+ return signature(session, "ECDSA", publicKey, EcdsaCurveSpec.P256);
}
/**
@@ -333,16 +325,17 @@ public final class TagEngineBuilder implements Supplier> {
*
*
* Requires the BouncyCastle PQC provider and a registered "SPHINCS+" algorithm
- * in {@link CryptoAlgorithms}.
+ * in the supplied {@link ZeroEchoSession}.
*
*
* @param privateKey private signing key; must not be {@code null}
* @return a builder that produces SPHINCS+ signature engines in SIGN mode
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
- public static TagEngineBuilder sphincsPlusSign(final PrivateKey privateKey) {
+ public static TagEngineBuilder sphincsPlusSign(final ZeroEchoSession session,
+ final PrivateKey privateKey) {
Objects.requireNonNull(privateKey, PRIVATE_KEY);
- return signature("SPHINCS+", privateKey, VoidSpec.INSTANCE);
+ return signature(session, "SPHINCS+", privateKey, VoidSpec.INSTANCE);
}
/**
@@ -350,16 +343,17 @@ public final class TagEngineBuilder implements Supplier> {
*
*
* Requires the BouncyCastle PQC provider and a registered "SPHINCS+" algorithm
- * in {@link CryptoAlgorithms}.
+ * in the supplied {@link ZeroEchoSession}.
*
*
* @param publicKey public verification key; must not be {@code null}
* @return a builder that produces SPHINCS+ signature engines in VERIFY mode
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
- public static TagEngineBuilder sphincsPlusVerify(final PublicKey publicKey) {
+ public static TagEngineBuilder sphincsPlusVerify(final ZeroEchoSession session,
+ final PublicKey publicKey) {
Objects.requireNonNull(publicKey, PUBLIC_KEY);
- return signature("SPHINCS+", publicKey, VoidSpec.INSTANCE);
+ return signature(session, "SPHINCS+", publicKey, VoidSpec.INSTANCE);
}
/**
@@ -368,16 +362,17 @@ public final class TagEngineBuilder implements Supplier> {
*
* SLH-DSA is the NIST-standardized hash-based signature scheme (FIPS 205). The
* concrete parameter set is encoded in the key material and interpreted by the
- * underlying {@link CryptoAlgorithms} implementation.
+ * algorithm resolved through the supplied {@link ZeroEchoSession}.
*
*
* @param privateKey private signing key; must not be {@code null}
* @return a builder that produces SLH-DSA signature engines in SIGN mode
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
- public static TagEngineBuilder slhDsaSign(final PrivateKey privateKey) {
+ public static TagEngineBuilder slhDsaSign(final ZeroEchoSession session,
+ final PrivateKey privateKey) {
Objects.requireNonNull(privateKey, PRIVATE_KEY);
- return signature("SLH-DSA", privateKey, VoidSpec.INSTANCE);
+ return signature(session, "SLH-DSA", privateKey, VoidSpec.INSTANCE);
}
/**
@@ -386,16 +381,17 @@ public final class TagEngineBuilder implements Supplier> {
*
* SLH-DSA is the NIST-standardized hash-based signature scheme (FIPS 205). The
* concrete parameter set is encoded in the key material and interpreted by the
- * underlying {@link CryptoAlgorithms} implementation.
+ * algorithm resolved through the supplied {@link ZeroEchoSession}.
*
*
* @param publicKey public verification key; must not be {@code null}
* @return a builder that produces SLH-DSA signature engines in VERIFY mode
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
- public static TagEngineBuilder slhDsaVerify(final PublicKey publicKey) {
+ public static TagEngineBuilder slhDsaVerify(final ZeroEchoSession session,
+ final PublicKey publicKey) {
Objects.requireNonNull(publicKey, PUBLIC_KEY);
- return signature("SLH-DSA", publicKey, VoidSpec.INSTANCE);
+ return signature(session, "SLH-DSA", publicKey, VoidSpec.INSTANCE);
}
/**
@@ -404,17 +400,18 @@ public final class TagEngineBuilder implements Supplier> {
*
* ML-DSA is the NIST-standardized module-lattice signature scheme (FIPS 204).
* The concrete parameter set and any pre-hash variant is encoded in the key
- * material and interpreted by the underlying {@link CryptoAlgorithms}
- * implementation.
+ * material and interpreted by the algorithm resolved through the supplied
+ * {@link ZeroEchoSession}.
*
*
* @param privateKey private signing key; must not be {@code null}
* @return a builder that produces ML-DSA signature engines in SIGN mode
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
- public static TagEngineBuilder mldsaSign(final PrivateKey privateKey) {
+ public static TagEngineBuilder mldsaSign(final ZeroEchoSession session,
+ final PrivateKey privateKey) {
Objects.requireNonNull(privateKey, PRIVATE_KEY);
- return signature("ML-DSA", privateKey, VoidSpec.INSTANCE);
+ return signature(session, "ML-DSA", privateKey, VoidSpec.INSTANCE);
}
/**
@@ -423,16 +420,17 @@ public final class TagEngineBuilder implements Supplier> {
*
* ML-DSA is the NIST-standardized module-lattice signature scheme (FIPS 204).
* The concrete parameter set and any pre-hash variant is encoded in the key
- * material and interpreted by the underlying {@link CryptoAlgorithms}
- * implementation.
+ * material and interpreted by the algorithm resolved through the supplied
+ * {@link ZeroEchoSession}.
*
*
* @param publicKey public verification key; must not be {@code null}
* @return a builder that produces ML-DSA signature engines in VERIFY mode
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
- public static TagEngineBuilder mldsaVerify(final PublicKey publicKey) {
+ public static TagEngineBuilder mldsaVerify(final ZeroEchoSession session,
+ final PublicKey publicKey) {
Objects.requireNonNull(publicKey, PUBLIC_KEY);
- return signature("ML-DSA", publicKey, VoidSpec.INSTANCE);
+ return signature(session, "ML-DSA", publicKey, VoidSpec.INSTANCE);
}
}
diff --git a/lib/src/main/java/zeroecho/core/util/GenerateCryptoCatalogTable.java b/lib/src/main/java/zeroecho/core/util/GenerateCryptoCatalogTable.java
index 2d678e6..435d203 100644
--- a/lib/src/main/java/zeroecho/core/util/GenerateCryptoCatalogTable.java
+++ b/lib/src/main/java/zeroecho/core/util/GenerateCryptoCatalogTable.java
@@ -37,17 +37,15 @@ import java.io.File;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Paths;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.ServiceLoader;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import zeroecho.core.Capability;
import zeroecho.core.CryptoAlgorithm;
+import zeroecho.core.CryptoAlgorithms;
import zeroecho.sdk.util.BouncyCastleActivator;
/**
@@ -92,14 +90,14 @@ public final class GenerateCryptoCatalogTable {
File out = new File(args[0]);
out.getParentFile().mkdirs();
- // Discover providers directly so we do not rely on CryptoCatalog internals.
- Map algos = loadAlgorithms();
- validate(algos);
+ Set algorithmIds = CryptoAlgorithms.available();
+ validate(algorithmIds);
// Enumerate subcolumns actually present.
SortedSet families = new TreeSet<>();
SortedSet roles = new TreeSet<>();
- for (CryptoAlgorithm a : algos.values()) {
+ for (String algorithmId : algorithmIds) {
+ CryptoAlgorithm a = CryptoAlgorithms.require(algorithmId);
for (Capability c : a.listCapabilities()) {
families.add(c.family().name());
roles.add(c.role().name());
@@ -144,8 +142,8 @@ public final class GenerateCryptoCatalogTable {
// TBODY same as before, but make family/role cells narrow and centered
w.write("");
- for (String id : new TreeSet<>(algos.keySet())) {
- CryptoAlgorithm a = algos.get(id);
+ for (String id : algorithmIds) {
+ CryptoAlgorithm a = CryptoAlgorithms.require(id);
Set famHit = new HashSet<>(); // NOPMD
Set roleHit = new HashSet<>(); // NOPMD
@@ -156,7 +154,7 @@ public final class GenerateCryptoCatalogTable {
roleHit.add(c.role().name());
if (c.defaultSpec() != null) {
try {
- Object ds = c.defaultSpec().get();
+ Object ds = c.defaultSpec();
if (ds != null) {
defaultSpecs.add(esc(labelOf(ds)));
}
@@ -189,24 +187,13 @@ public final class GenerateCryptoCatalogTable {
}
}
- private static Map loadAlgorithms() {
- Map m = new HashMap<>();
- ServiceLoader.load(CryptoAlgorithm.class).forEach(a -> {
- CryptoAlgorithm prev = m.put(a.id(), a);
- if (prev != null) {
- throw new IllegalStateException("Duplicate algorithm id: " + a.id());
- }
- });
- return m;
- }
-
- private static void validate(Map algos) {
+ private static void validate(Set algorithmIds) {
StringBuilder sb = null;
- for (CryptoAlgorithm a : algos.values()) {
+ for (String algorithmId : algorithmIds) {
+ CryptoAlgorithm a = CryptoAlgorithms.require(algorithmId);
boolean hasCaps = !a.listCapabilities().isEmpty();
- boolean hasAsym = !a.asymmetricBuildersInfo().isEmpty();
- boolean hasSym = !a.symmetricBuildersInfo().isEmpty();
- if (!hasCaps && !hasAsym && !hasSym) {
+ boolean hasKeyOperations = !a.keyOperations().isEmpty();
+ if (!hasCaps && !hasKeyOperations) {
if (sb == null) {
sb = new StringBuilder(); // NOPMD
}
diff --git a/lib/src/main/java/zeroecho/sdk/KeyBuilders.java b/lib/src/main/java/zeroecho/sdk/KeyBuilders.java
new file mode 100644
index 0000000..978d91e
--- /dev/null
+++ b/lib/src/main/java/zeroecho/sdk/KeyBuilders.java
@@ -0,0 +1,271 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the conditions in the project LICENSE are met.
+ ******************************************************************************/
+package zeroecho.sdk;
+
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.Objects;
+
+import javax.crypto.SecretKey;
+
+import zeroecho.core.CryptoAlgorithm;
+import zeroecho.core.spec.AlgorithmKeySpec;
+import zeroecho.core.spi.AsymmetricKeyPairGenerator;
+import zeroecho.core.spi.PrivateKeyImporter;
+import zeroecho.core.spi.PublicKeyImporter;
+import zeroecho.core.spi.SymmetricKeyGenerator;
+import zeroecho.core.spi.SymmetricKeyImporter;
+
+/**
+ * Session-bound entry point for exact key-material operations.
+ *
+ * Capability lookup fails before an operation object is returned. Returned
+ * objects guarantee the requested operation and report successful execution to
+ * the owning session's audit listener on a best-effort basis.
+ *
+ * @since 1.0
+ */
+public final class KeyBuilders {
+ private final ZeroEchoSession session;
+ private final Symmetric symmetric = new Symmetric();
+ private final Asymmetric asymmetric = new Asymmetric();
+
+ /* default */ KeyBuilders(ZeroEchoSession session) {
+ this.session = Objects.requireNonNull(session, "session must not be null");
+ }
+
+ /**
+ * Returns symmetric key operations.
+ *
+ * @return session-bound symmetric namespace
+ */
+ public Symmetric symmetric() {
+ return symmetric;
+ }
+
+ /**
+ * Returns asymmetric key operations.
+ *
+ * @return session-bound asymmetric namespace
+ */
+ public Asymmetric asymmetric() {
+ return asymmetric;
+ }
+
+ /**
+ * Symmetric generation and import lookups.
+ */
+ public final class Symmetric {
+ private Symmetric() {
+ }
+
+ /**
+ * Resolves an exact symmetric generator.
+ *
+ * @param algorithmId canonical algorithm identifier
+ * @param specType exact specification class
+ * @param specification type
+ * @return guaranteed generator
+ * @throws IllegalArgumentException if the capability is absent
+ */
+ public SymmetricKeyGenerator generator(String algorithmId,
+ Class specType) {
+ CryptoAlgorithm algorithm = session.require(algorithmId);
+ SymmetricKeyGenerator delegate = algorithm.symmetricKeyGenerator(specType);
+ return spec -> {
+ SecretKey key = delegate.generateSecret(spec);
+ session.notifyKeyGenerated(algorithm, spec, key);
+ return key;
+ };
+ }
+
+ /**
+ * Resolves an exact symmetric importer.
+ *
+ * @param algorithmId canonical algorithm identifier
+ * @param specType exact specification class
+ * @param specification type
+ * @return guaranteed importer
+ * @throws IllegalArgumentException if the capability is absent
+ */
+ public SymmetricKeyImporter importer(String algorithmId,
+ Class specType) {
+ CryptoAlgorithm algorithm = session.require(algorithmId);
+ SymmetricKeyImporter delegate = algorithm.symmetricKeyImporter(specType);
+ return spec -> {
+ SecretKey key = delegate.importSecret(spec);
+ session.notifyKeyBuilt(algorithm, spec, key);
+ return key;
+ };
+ }
+
+ /**
+ * Generates a symmetric key using the exact runtime specification type.
+ *
+ * @param algorithmId canonical algorithm identifier
+ * @param spec generation specification
+ * @param specification type
+ * @return generated secret key
+ * @throws java.security.GeneralSecurityException if generation fails
+ * @throws IllegalArgumentException if the capability is absent
+ * @throws NullPointerException if {@code spec} is {@code null}
+ */
+ public SecretKey generate(String algorithmId, S spec)
+ throws java.security.GeneralSecurityException {
+ Objects.requireNonNull(spec, "spec");
+ @SuppressWarnings("unchecked")
+ Class specType = (Class) spec.getClass();
+ return generator(algorithmId, specType).generateSecret(spec);
+ }
+
+ /**
+ * Imports a symmetric key using the exact runtime specification type.
+ *
+ * @param algorithmId canonical algorithm identifier
+ * @param spec import specification
+ * @param specification type
+ * @return imported secret key
+ * @throws java.security.GeneralSecurityException if import fails
+ * @throws IllegalArgumentException if the capability is absent
+ * @throws NullPointerException if {@code spec} is {@code null}
+ */
+ public SecretKey importKey(String algorithmId, S spec)
+ throws java.security.GeneralSecurityException {
+ Objects.requireNonNull(spec, "spec");
+ @SuppressWarnings("unchecked")
+ Class specType = (Class) spec.getClass();
+ return importer(algorithmId, specType).importSecret(spec);
+ }
+ }
+
+ /**
+ * Asymmetric generation and import lookups.
+ */
+ public final class Asymmetric {
+ private Asymmetric() {
+ }
+
+ /**
+ * Resolves an exact key-pair generator.
+ *
+ * @param algorithmId canonical algorithm identifier
+ * @param specType exact specification class
+ * @param specification type
+ * @return guaranteed generator
+ * @throws IllegalArgumentException if the capability is absent
+ */
+ public AsymmetricKeyPairGenerator keyPairGenerator(String algorithmId,
+ Class specType) {
+ CryptoAlgorithm algorithm = session.require(algorithmId);
+ AsymmetricKeyPairGenerator delegate = algorithm.asymmetricKeyPairGenerator(specType);
+ return spec -> {
+ KeyPair pair = delegate.generateKeyPair(spec);
+ session.notifyKeyPairGenerated(algorithm, spec, pair);
+ return pair;
+ };
+ }
+
+ /**
+ * Resolves an exact public-key importer.
+ *
+ * @param algorithmId canonical algorithm identifier
+ * @param specType exact specification class
+ * @param specification type
+ * @return guaranteed importer
+ * @throws IllegalArgumentException if the capability is absent
+ */
+ public PublicKeyImporter publicImporter(String algorithmId,
+ Class specType) {
+ CryptoAlgorithm algorithm = session.require(algorithmId);
+ PublicKeyImporter delegate = algorithm.publicKeyImporter(specType);
+ return spec -> {
+ PublicKey key = delegate.importPublic(spec);
+ session.notifyKeyBuilt(algorithm, spec, key);
+ return key;
+ };
+ }
+
+ /**
+ * Resolves an exact private-key importer.
+ *
+ * @param algorithmId canonical algorithm identifier
+ * @param specType exact specification class
+ * @param specification type
+ * @return guaranteed importer
+ * @throws IllegalArgumentException if the capability is absent
+ */
+ public PrivateKeyImporter privateImporter(String algorithmId,
+ Class specType) {
+ CryptoAlgorithm algorithm = session.require(algorithmId);
+ PrivateKeyImporter delegate = algorithm.privateKeyImporter(specType);
+ return spec -> {
+ PrivateKey key = delegate.importPrivate(spec);
+ session.notifyKeyBuilt(algorithm, spec, key);
+ return key;
+ };
+ }
+
+ /**
+ * Generates a key pair using the exact runtime specification type.
+ *
+ * @param algorithmId canonical algorithm identifier
+ * @param spec generation specification
+ * @param specification type
+ * @return generated key pair
+ * @throws java.security.GeneralSecurityException if generation fails
+ * @throws IllegalArgumentException if the capability is absent
+ * @throws NullPointerException if {@code spec} is {@code null}
+ */
+ public KeyPair generateKeyPair(String algorithmId, S spec)
+ throws java.security.GeneralSecurityException {
+ Objects.requireNonNull(spec, "spec");
+ @SuppressWarnings("unchecked")
+ Class specType = (Class) spec.getClass();
+ return keyPairGenerator(algorithmId, specType).generateKeyPair(spec);
+ }
+
+ /**
+ * Imports a public key using the exact runtime specification type.
+ *
+ * @param algorithmId canonical algorithm identifier
+ * @param spec public-key import specification
+ * @param specification type
+ * @return imported public key
+ * @throws java.security.GeneralSecurityException if import fails
+ * @throws IllegalArgumentException if the capability is absent
+ * @throws NullPointerException if {@code spec} is {@code null}
+ */
+ public PublicKey importPublic(String algorithmId, S spec)
+ throws java.security.GeneralSecurityException {
+ Objects.requireNonNull(spec, "spec");
+ @SuppressWarnings("unchecked")
+ Class specType = (Class) spec.getClass();
+ return publicImporter(algorithmId, specType).importPublic(spec);
+ }
+
+ /**
+ * Imports a private key using the exact runtime specification type.
+ *
+ * @param algorithmId canonical algorithm identifier
+ * @param spec private-key import specification
+ * @param specification type
+ * @return imported private key
+ * @throws java.security.GeneralSecurityException if import fails
+ * @throws IllegalArgumentException if the capability is absent
+ * @throws NullPointerException if {@code spec} is {@code null}
+ */
+ public PrivateKey importPrivate(String algorithmId, S spec)
+ throws java.security.GeneralSecurityException {
+ Objects.requireNonNull(spec, "spec");
+ @SuppressWarnings("unchecked")
+ Class specType = (Class) spec.getClass();
+ return privateImporter(algorithmId, specType).importPrivate(spec);
+ }
+ }
+}
diff --git a/lib/src/main/java/zeroecho/sdk/Pbkdf2Limits.java b/lib/src/main/java/zeroecho/sdk/Pbkdf2Limits.java
new file mode 100644
index 0000000..3cb708d
--- /dev/null
+++ b/lib/src/main/java/zeroecho/sdk/Pbkdf2Limits.java
@@ -0,0 +1,65 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the conditions in the project LICENSE are met.
+ ******************************************************************************/
+package zeroecho.sdk;
+
+/**
+ * Explicit PBKDF2 work-factor limits for trusted configuration and decoded data.
+ *
+ * @param operationalMaximum largest iteration count accepted from trusted local
+ * configuration
+ * @param absoluteDecodedMaximum hard safety ceiling for untrusted decoded data
+ * @since 1.0
+ */
+public record Pbkdf2Limits(int operationalMaximum, int absoluteDecodedMaximum) {
+ /** Mandatory minimum PBKDF2 iteration count. */
+ public static final int MINIMUM = 10_000;
+
+ /**
+ * Validates {@code minimum <= operationalMaximum <= absoluteDecodedMaximum}.
+ *
+ * @throws IllegalArgumentException if the limits violate the ordering
+ */
+ public Pbkdf2Limits {
+ if (operationalMaximum < MINIMUM) {
+ throw new IllegalArgumentException("operationalMaximum must be at least " + MINIMUM);
+ }
+ if (absoluteDecodedMaximum < operationalMaximum) {
+ throw new IllegalArgumentException("absoluteDecodedMaximum must be at least operationalMaximum");
+ }
+ }
+
+ /**
+ * Validates trusted local configuration.
+ *
+ * @param iterations requested iteration count
+ * @throws IllegalArgumentException if outside the operational range
+ */
+ public void validateTrusted(int iterations) {
+ if (iterations < MINIMUM || iterations > operationalMaximum) {
+ throw new IllegalArgumentException("PBKDF2 iterations must be in range " + MINIMUM + ".."
+ + operationalMaximum + ": " + iterations);
+ }
+ }
+
+ /**
+ * Validates an untrusted decoded iteration count before KDF execution.
+ *
+ * @param iterations decoded iteration count
+ * @throws IllegalArgumentException if outside the absolute safety range
+ */
+ public void validateDecoded(int iterations) {
+ if (iterations < MINIMUM || iterations > absoluteDecodedMaximum) {
+ throw new IllegalArgumentException("Decoded PBKDF2 iterations must be in range " + MINIMUM + ".."
+ + absoluteDecodedMaximum + ": " + iterations);
+ }
+ if (iterations > operationalMaximum) {
+ throw new IllegalArgumentException("Decoded PBKDF2 iterations exceed the session policy maximum "
+ + operationalMaximum + ": " + iterations);
+ }
+ }
+}
diff --git a/lib/src/main/java/zeroecho/sdk/ZeroEchoSession.java b/lib/src/main/java/zeroecho/sdk/ZeroEchoSession.java
new file mode 100644
index 0000000..b0beb7b
--- /dev/null
+++ b/lib/src/main/java/zeroecho/sdk/ZeroEchoSession.java
@@ -0,0 +1,382 @@
+/*******************************************************************************
+ * Copyright (C) 2026, Leo Galambos
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. All advertising materials mentioning features or use of this software must
+ * display the following acknowledgement:
+ * This product includes software developed by the Egothor project.
+ *
+ * 4. Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ ******************************************************************************/
+package zeroecho.sdk;
+
+import java.security.Key;
+import java.security.KeyPair;
+import java.util.Objects;
+import java.util.Set;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.security.auth.DestroyFailedException;
+import javax.security.auth.Destroyable;
+
+import zeroecho.core.CryptoAlgorithm;
+import zeroecho.core.CryptoAlgorithms;
+import zeroecho.core.KeyUsage;
+import zeroecho.core.audit.AuditListener;
+import zeroecho.core.audit.AuditMode;
+import zeroecho.core.audit.AuditedContexts;
+import zeroecho.core.audit.AuditListeners;
+import zeroecho.core.context.AgreementContext;
+import zeroecho.core.context.CryptoContext;
+import zeroecho.core.context.DigestContext;
+import zeroecho.core.context.EncryptionContext;
+import zeroecho.core.context.KemContext;
+import zeroecho.core.context.MacContext;
+import zeroecho.core.context.SignatureContext;
+import zeroecho.core.err.UnsupportedRoleException;
+import zeroecho.core.err.UnsupportedSpecException;
+import zeroecho.core.policy.CryptoPolicy;
+import zeroecho.core.spec.AlgorithmKeySpec;
+import zeroecho.core.spec.ContextSpec;
+
+/**
+ * Immutable, explicitly scoped runtime configuration for ZeroEcho operations.
+ *
+ *
+ * Each session owns one policy, audit listener, and audit mode. Configuration
+ * methods return a new session and never mutate the receiver, so independently
+ * created sessions cannot affect one another. The default configuration is a
+ * permissive policy, a no-op listener, and {@link AuditMode#OFF}.
+ *
+ *
+ *
+ * Sessions share the immutable provider registry owned by
+ * {@link CryptoAlgorithms}; provider discovery is therefore performed once for
+ * the registry lifecycle. Session instances are safe for concurrent use.
+ * Configured policy and listener implementations must themselves support any
+ * concurrency with which the session is used.
+ *
+ *
+ *
+ * Audit callbacks may receive key and specification objects. Listeners are
+ * trusted application components and must not log or retain keys, plaintext,
+ * seeds, shared secrets, or sensitive specifications.
+ *
+ *
+ * @since 1.0
+ */
+public final class ZeroEchoSession {
+ private static final int DESTROY_LOCK_STRIPES = 64;
+ private static final ReentrantLock[] DESTROY_LOCKS = createDestroyLocks();
+ private final CryptoPolicy policy;
+ private final AuditListener auditListener;
+ private final AuditListener auditSink;
+ private final AuditMode auditMode;
+ private final KeyBuilders keyBuilders;
+ private final Pbkdf2Limits pbkdf2Limits;
+
+ /**
+ * Creates a session with a permissive policy, no-op audit listener, and
+ * {@link AuditMode#OFF}.
+ */
+ public ZeroEchoSession() {
+ this(CryptoPolicy.permissive(), AuditListener.noop(), AuditMode.OFF, null);
+ }
+
+ private ZeroEchoSession(CryptoPolicy policy, AuditListener auditListener,
+ AuditMode auditMode, Pbkdf2Limits pbkdf2Limits) {
+ this.policy = Objects.requireNonNull(policy, "policy must not be null");
+ this.auditListener = Objects.requireNonNull(auditListener, "auditListener must not be null");
+ this.auditSink = AuditListeners.bestEffort(auditListener);
+ this.auditMode = Objects.requireNonNull(auditMode, "auditMode must not be null");
+ this.pbkdf2Limits = pbkdf2Limits;
+ this.keyBuilders = new KeyBuilders(this);
+ }
+
+ /**
+ * Returns a session with the supplied policy and this session's audit
+ * configuration.
+ *
+ * @param newPolicy policy applied before context creation; must not be
+ * {@code null}
+ * @return a new independently configured session
+ * @throws NullPointerException if {@code newPolicy} is {@code null}
+ */
+ public ZeroEchoSession withPolicy(CryptoPolicy newPolicy) {
+ return new ZeroEchoSession(Objects.requireNonNull(newPolicy, "newPolicy must not be null"), auditListener,
+ auditMode, pbkdf2Limits);
+ }
+
+ /**
+ * Returns a session with the supplied audit listener and this session's policy
+ * and audit mode.
+ *
+ * @param newAuditListener listener receiving audit callbacks; must not be
+ * {@code null}
+ * @return a new independently configured session
+ * @throws NullPointerException if {@code newAuditListener} is {@code null}
+ */
+ public ZeroEchoSession withAuditListener(AuditListener newAuditListener) {
+ return new ZeroEchoSession(policy,
+ Objects.requireNonNull(newAuditListener, "newAuditListener must not be null"), auditMode,
+ pbkdf2Limits);
+ }
+
+ /**
+ * Returns a session with the supplied audit mode and this session's policy and
+ * listener.
+ *
+ * @param newAuditMode audit strategy; must not be {@code null}
+ * @return a new independently configured session
+ * @throws NullPointerException if {@code newAuditMode} is {@code null}
+ */
+ public ZeroEchoSession withAuditMode(AuditMode newAuditMode) {
+ return new ZeroEchoSession(policy, auditListener,
+ Objects.requireNonNull(newAuditMode, "newAuditMode must not be null"), pbkdf2Limits);
+ }
+
+ /**
+ * Returns a session with explicit PBKDF2 work-factor limits.
+ *
+ * @param limits deployment-selected limits
+ * @return independently configured session
+ * @throws NullPointerException if {@code limits} is {@code null}
+ */
+ public ZeroEchoSession withPbkdf2Limits(Pbkdf2Limits limits) {
+ return new ZeroEchoSession(policy, auditListener, auditMode,
+ Objects.requireNonNull(limits, "limits must not be null"));
+ }
+
+ /**
+ * Returns configured PBKDF2 limits.
+ *
+ * @return explicit deployment limits
+ * @throws IllegalStateException if limits were not configured
+ */
+ public Pbkdf2Limits pbkdf2Limits() {
+ if (pbkdf2Limits == null) {
+ throw new IllegalStateException("PBKDF2 limits must be configured explicitly");
+ }
+ return pbkdf2Limits;
+ }
+
+ /**
+ * Returns the policy owned by this session.
+ *
+ * @return the non-null policy strategy
+ */
+ public CryptoPolicy policy() {
+ return policy;
+ }
+
+ /**
+ * Returns the audit listener owned by this session.
+ *
+ *
+ * The returned listener is the configured strategy, not mutable session
+ * state. It is exposed to support manual audit mode.
+ *
+ *
+ * @return the non-null audit listener
+ */
+ public AuditListener auditListener() {
+ return auditListener;
+ }
+
+ /**
+ * Returns the audit mode owned by this session.
+ *
+ * @return the non-null audit mode
+ */
+ public AuditMode auditMode() {
+ return auditMode;
+ }
+
+ /**
+ * Returns the available algorithm identifiers in deterministic registry order.
+ *
+ * @return an unmodifiable set of canonical algorithm identifiers
+ */
+ public Set available() {
+ return CryptoAlgorithms.available();
+ }
+
+ /**
+ * Resolves an algorithm from the authoritative registry.
+ *
+ * @param id canonical algorithm identifier
+ * @return the registered algorithm
+ * @throws IllegalArgumentException if no algorithm is registered under
+ * {@code id}
+ */
+ public CryptoAlgorithm require(String id) {
+ return CryptoAlgorithms.require(id);
+ }
+
+ /**
+ * Returns exact session-bound key operations.
+ *
+ * @return immutable grouped key-operation entry point
+ */
+ public KeyBuilders keyBuilders() {
+ return keyBuilders;
+ }
+
+ /**
+ * Creates a context after applying this session's policy and audit
+ * configuration.
+ *
+ * @param id canonical algorithm identifier
+ * @param role intended key usage
+ * @param key key compatible with the selected algorithm and role
+ * @param spec optional context specification, or {@code null} for the
+ * algorithm default
+ * @param context type
+ * @param key type
+ * @param context specification type
+ * @return a ready context, possibly audit-wrapped in {@link AuditMode#WRAP}
+ * @throws IllegalArgumentException if the algorithm identifier is unknown or
+ * policy validation rejects the operation
+ * @throws UnsupportedRoleException if the algorithm does not support
+ * {@code role}
+ * @throws UnsupportedSpecException if the key or specification is incompatible
+ */
+ public C createContext(String id, KeyUsage role,
+ K key, S spec) {
+ policy.validate(id, role, key, spec);
+
+ CryptoAlgorithm algorithm = require(id);
+ C context = algorithm.createContext(role, key, spec);
+ return finishContext(algorithm, context, role, spec);
+ }
+
+ private C finishContext(CryptoAlgorithm algorithm,
+ C context, KeyUsage role, S spec) {
+ if (auditMode == AuditMode.OFF) {
+ notifyContextCreated(algorithm, role, spec);
+ return context;
+ }
+ if (auditMode == AuditMode.WRAP) {
+ return wrapForAudit(context, role);
+ }
+ return context;
+ }
+
+ /**
+ * Creates a context using the selected algorithm's default specification.
+ *
+ * @param id canonical algorithm identifier
+ * @param role intended key usage
+ * @param key key compatible with the selected algorithm and role
+ * @param context type
+ * @param key type
+ * @return a ready context, possibly audit-wrapped in {@link AuditMode#WRAP}
+ * @throws IllegalArgumentException if the algorithm identifier is unknown or
+ * policy validation rejects the operation
+ * @throws UnsupportedRoleException if the algorithm does not support
+ * {@code role}
+ */
+ public C createContext(String id, KeyUsage role, K key) {
+ return createContext(id, role, key, null);
+ }
+
+ @SuppressWarnings("unchecked")
+ private C wrapForAudit(C context, KeyUsage role) {
+ return (C) switch (context) {
+ case SignatureContext signatureContext -> AuditedContexts.wrap(signatureContext, auditSink, role);
+ case EncryptionContext encryptionContext -> AuditedContexts.wrap(encryptionContext, auditSink, role);
+ case KemContext kemContext -> AuditedContexts.wrap(kemContext, auditSink, role);
+ case DigestContext digestContext -> AuditedContexts.wrap(digestContext, auditSink, role);
+ case MacContext macContext -> AuditedContexts.wrap(macContext, auditSink, role);
+ case AgreementContext agreementContext -> AuditedContexts.wrap(agreementContext, auditSink, role);
+ };
+ }
+
+ /**
+ * Destroys a key and verifies that it entered the destroyed state.
+ *
+ * @param algorithmId algorithm identifier used as audit metadata
+ * @param provider provider name used as audit metadata
+ * @param key key to destroy; must not be {@code null}
+ * @return {@code true} only when this call transitions the key to destroyed;
+ * {@code false} for a non-destroyable or already destroyed key
+ * @throws NullPointerException if {@code key} is {@code null}
+ * @throws DestroyFailedException if destruction fails or the key does not
+ * report itself destroyed afterward
+ * @throws RuntimeException if the key's lifecycle implementation throws one
+ */
+ public boolean destroyKey(String algorithmId, String provider, Key key) throws DestroyFailedException {
+ Objects.requireNonNull(key, "key must not be null");
+ if (!(key instanceof Destroyable destroyable)) {
+ return false;
+ }
+ ReentrantLock destroyLock = DESTROY_LOCKS[Math.floorMod(System.identityHashCode(key), DESTROY_LOCKS.length)];
+ destroyLock.lock();
+ try {
+ if (destroyable.isDestroyed()) {
+ return false;
+ }
+ destroyable.destroy();
+ if (!destroyable.isDestroyed()) {
+ throw new DestroyFailedException("Key did not enter the destroyed state");
+ }
+ } finally {
+ destroyLock.unlock();
+ }
+ auditSink.onKeyDestroyed(algorithmId, provider, key);
+ return true;
+ }
+
+ private void notifyContextCreated(CryptoAlgorithm algorithm,
+ KeyUsage role, S spec) {
+ Map metadata = spec == null ? Map.of()
+ : Map.of("specType", spec.getClass().getName());
+ auditSink.onContextCreatedMeta(UUID.randomUUID().toString(), algorithm.id(), algorithm.providerName(),
+ role, "n/a", metadata);
+ }
+
+ /* default */ void notifyKeyPairGenerated(CryptoAlgorithm algorithm, AlgorithmKeySpec spec, KeyPair keyPair) {
+ auditSink.onKeyGenerated(algorithm.id(), algorithm.providerName(), spec, keyPair);
+ }
+
+ /* default */ void notifyKeyGenerated(CryptoAlgorithm algorithm, AlgorithmKeySpec spec, Key key) {
+ notifyKeyBuilt(algorithm, spec, key);
+ }
+
+ /* default */ void notifyKeyBuilt(CryptoAlgorithm algorithm, AlgorithmKeySpec spec, Key key) {
+ auditSink.onKeyBuilt(algorithm.id(), algorithm.providerName(), spec, key);
+ }
+
+ private static ReentrantLock[] createDestroyLocks() {
+ ReentrantLock[] locks = new ReentrantLock[DESTROY_LOCK_STRIPES];
+ for (int index = 0; index < locks.length; index++) {
+ locks[index] = new ReentrantLock();
+ }
+ return locks;
+ }
+}
diff --git a/lib/src/main/java/zeroecho/sdk/builders/HybridKexBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/HybridKexBuilder.java
index c5e3791..9b399ed 100644
--- a/lib/src/main/java/zeroecho/sdk/builders/HybridKexBuilder.java
+++ b/lib/src/main/java/zeroecho/sdk/builders/HybridKexBuilder.java
@@ -33,12 +33,13 @@
******************************************************************************/
package zeroecho.sdk.builders;
+import zeroecho.sdk.ZeroEchoSession;
+
import java.io.IOException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Objects;
-import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.alg.common.agreement.KeyPairKey;
import zeroecho.core.context.AgreementContext;
@@ -89,6 +90,7 @@ import zeroecho.sdk.hybrid.kex.HybridKexTranscript;
*/
public final class HybridKexBuilder {
+ private final ZeroEchoSession session;
private HybridKexProfile profile;
private HybridKexTranscript transcript;
private HybridKexPolicy policy;
@@ -106,17 +108,19 @@ public final class HybridKexBuilder {
private PublicKey pqcPeerPublic;
private PrivateKey pqcPrivate;
- private HybridKexBuilder() {
- // builder
+ private HybridKexBuilder(ZeroEchoSession session) {
+ this.session = Objects.requireNonNull(session, "session");
}
/**
* Creates a new builder instance.
*
+ * @param session explicit runtime configuration
* @return new builder
+ * @throws NullPointerException if {@code session} is {@code null}
*/
- public static HybridKexBuilder builder() {
- return new HybridKexBuilder();
+ public static HybridKexBuilder builder(ZeroEchoSession session) {
+ return new HybridKexBuilder(session);
}
/**
@@ -245,26 +249,23 @@ public final class HybridKexBuilder {
*
* @return classic agreement context derived from the configured classic-leg
* state
- * @throws IOException if underlying context creation fails
* @throws IllegalStateException if the selected classic mode is missing
* required state
*/
- private AgreementContext buildClassicLeg() throws IOException {
+ private AgreementContext buildClassicLeg() {
if (classicMode == ClassicMode.CLASSIC_AGREEMENT) {
- if (classicPrivate == null || classicPeerPublic == null) {
- throw new IllegalStateException(
- "classic private key and peer public must be set for CLASSIC_AGREEMENT");
- }
- AgreementContext classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicPrivate,
+ AgreementContext classic = session.createContext(classicAlgId, KeyUsage.AGREEMENT, classicPrivate,
classicSpec);
- classic.setPeerPublic(classicPeerPublic);
- return classic;
+ try {
+ classic.setPeerPublic(classicPeerPublic);
+ return classic;
+ } catch (RuntimeException | Error failure) { // NOPMD - close context on unchecked failure
+ closeAfterFailure(classic, failure);
+ throw failure;
+ }
}
if (classicMode == ClassicMode.PAIR_MESSAGE) {
- if (classicKeyPair == null) {
- throw new IllegalStateException("classic key pair must be set for PAIR_MESSAGE");
- }
- return CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicKeyPair, classicSpec);
+ return session.createContext(classicAlgId, KeyUsage.AGREEMENT, classicKeyPair, classicSpec);
}
throw new IllegalStateException("classic mode must be selected");
}
@@ -273,46 +274,48 @@ public final class HybridKexBuilder {
* Builds initiator-side context.
*
* @return initiator context
- * @throws IOException if underlying context creation fails
*/
- public HybridKexContext buildInitiator() throws IOException {
- validateCommon();
-
- AgreementContext classic = buildClassicLeg();
-
- if (pqcPeerPublic == null) {
- throw new IllegalStateException("pqc peer public must be set for initiator");
- }
- MessageAgreementContext pqc = CryptoAlgorithms.create(pqcAlgId, KeyUsage.AGREEMENT, pqcPeerPublic, pqcSpec);
-
+ public HybridKexContext buildInitiator() {
+ validateInitiator();
HybridKexProfile effective = effectiveProfile();
- if (policy != null) {
- policy.enforce(effective, classic, pqc);
+ AgreementContext classic = null;
+ MessageAgreementContext pqc = null;
+ try {
+ classic = buildClassicLeg();
+ pqc = session.createContext(pqcAlgId, KeyUsage.AGREEMENT, pqcPeerPublic, pqcSpec);
+ if (policy != null) {
+ policy.enforce(effective, classic, pqc);
+ }
+ return new HybridKexContext(effective, classic, pqc);
+ } catch (RuntimeException | Error failure) { // NOPMD - close partial construction
+ closeAfterFailure(pqc, failure);
+ closeAfterFailure(classic, failure);
+ throw failure;
}
- return new HybridKexContext(effective, classic, pqc);
}
/**
* Builds responder-side context.
*
* @return responder context
- * @throws IOException if underlying context creation fails
*/
- public HybridKexContext buildResponder() throws IOException {
- validateCommon();
-
- AgreementContext classic = buildClassicLeg();
-
- if (pqcPrivate == null) {
- throw new IllegalStateException("pqc private key must be set for responder");
- }
- MessageAgreementContext pqc = CryptoAlgorithms.create(pqcAlgId, KeyUsage.AGREEMENT, pqcPrivate, pqcSpec);
-
+ public HybridKexContext buildResponder() {
+ validateResponder();
HybridKexProfile effective = effectiveProfile();
- if (policy != null) {
- policy.enforce(effective, classic, pqc);
+ AgreementContext classic = null;
+ MessageAgreementContext pqc = null;
+ try {
+ classic = buildClassicLeg();
+ pqc = session.createContext(pqcAlgId, KeyUsage.AGREEMENT, pqcPrivate, pqcSpec);
+ if (policy != null) {
+ policy.enforce(effective, classic, pqc);
+ }
+ return new HybridKexContext(effective, classic, pqc);
+ } catch (RuntimeException | Error failure) { // NOPMD - close partial construction
+ closeAfterFailure(pqc, failure);
+ closeAfterFailure(classic, failure);
+ throw failure;
}
- return new HybridKexContext(effective, classic, pqc);
}
/**
@@ -340,6 +343,39 @@ public final class HybridKexBuilder {
if (pqcAlgId == null) {
throw new IllegalStateException("pqc algorithm id must be set");
}
+ if (classicMode == ClassicMode.CLASSIC_AGREEMENT
+ && (classicPrivate == null || classicPeerPublic == null)) {
+ throw new IllegalStateException(
+ "classic private key and peer public must be set for CLASSIC_AGREEMENT");
+ }
+ if (classicMode == ClassicMode.PAIR_MESSAGE && classicKeyPair == null) {
+ throw new IllegalStateException("classic key pair must be set for PAIR_MESSAGE");
+ }
+ }
+
+ private void validateInitiator() {
+ validateCommon();
+ if (pqcPeerPublic == null) {
+ throw new IllegalStateException("pqc peer public must be set for initiator");
+ }
+ }
+
+ private void validateResponder() {
+ validateCommon();
+ if (pqcPrivate == null) {
+ throw new IllegalStateException("pqc private key must be set for responder");
+ }
+ }
+
+ private static void closeAfterFailure(zeroecho.core.context.CryptoContext context, Throwable failure) {
+ if (context == null) {
+ return;
+ }
+ try {
+ context.close();
+ } catch (IOException | RuntimeException closeFailure) { // NOPMD - preserve close failure
+ failure.addSuppressed(closeFailure);
+ }
}
private HybridKexProfile effectiveProfile() {
@@ -693,12 +729,11 @@ public final class HybridKexBuilder {
* configuration.
*
* @return initiator context
- * @throws IOException if underlying context creation fails
* @throws IllegalStateException if required configuration for initiator role is
* missing
* @since 1.0
*/
- public HybridKexContext buildInitiator() throws IOException {
+ public HybridKexContext buildInitiator() {
return parent.buildInitiator();
}
@@ -707,12 +742,11 @@ public final class HybridKexBuilder {
* configuration.
*
* @return responder context
- * @throws IOException if underlying context creation fails
* @throws IllegalStateException if required configuration for responder role is
* missing
* @since 1.0
*/
- public HybridKexContext buildResponder() throws IOException {
+ public HybridKexContext buildResponder() {
return parent.buildResponder();
}
}
diff --git a/lib/src/main/java/zeroecho/sdk/builders/SignatureTrailerDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/SignatureTrailerDataContentBuilder.java
index 28fb945..b26119b 100644
--- a/lib/src/main/java/zeroecho/sdk/builders/SignatureTrailerDataContentBuilder.java
+++ b/lib/src/main/java/zeroecho/sdk/builders/SignatureTrailerDataContentBuilder.java
@@ -33,7 +33,8 @@
******************************************************************************/
package zeroecho.sdk.builders;
-import java.io.IOException;
+import zeroecho.sdk.ZeroEchoSession;
+
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
@@ -42,7 +43,6 @@ import java.util.function.Supplier;
import conflux.CtxInterface;
import conflux.Key;
-import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.spec.ContextSpec;
import zeroecho.core.tag.TagEngine;
@@ -75,19 +75,15 @@ import zeroecho.sdk.hybrid.signature.HybridSignatureProfile;
*
* - {@link #core(TagEngine)} / {@link #core(Supplier)}: wraps a ready engine
* (same parameters as {@link TagTrailerDataContentBuilder}).
- * - {@link #single()}: constructs a non-hybrid {@code SignatureContext} via
- * {@link CryptoAlgorithms}.
- * - {@link #hybrid()}: constructs a hybrid {@code SignatureContext} via
+ *
- {@link #single(ZeroEchoSession)}: constructs a non-hybrid
+ * {@code SignatureContext}.
+ * - {@link #hybrid(ZeroEchoSession)}: constructs a hybrid
+ * {@code SignatureContext} via
* {@link HybridSignatureContexts}.
*
*
- * Checked exceptions
- *
- * Context construction may involve I/O (e.g., catalog/provider loading) and
- * therefore throw {@link IOException}. This builder converts such failures to
- * {@link IllegalStateException} because fluent builder APIs are expected to be
- * used in configuration code without mandatory checked-exception plumbing.
- *
+ * Context construction is in-memory. Checked I/O failures arise only when a
+ * built stream is attached or processed.
*
* @since 1.0
*/
@@ -146,8 +142,8 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil
* @return selector for creating signing/verifying builders
* @since 1.0
*/
- public static SingleSelector single() {
- return new SingleSelector();
+ public static SingleSelector single(ZeroEchoSession session) {
+ return new SingleSelector(session);
}
/**
@@ -156,8 +152,8 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil
* @return selector for creating signing/verifying builders
* @since 1.0
*/
- public static HybridSelector hybrid() {
- return new HybridSelector();
+ public static HybridSelector hybrid(ZeroEchoSession session) {
+ return new HybridSelector(session);
}
/**
@@ -218,7 +214,10 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil
*/
public static final class SingleSelector {
- private SingleSelector() {
+ private final ZeroEchoSession session;
+
+ private SingleSelector(ZeroEchoSession session) {
+ this.session = Objects.requireNonNull(session, "session");
}
/**
@@ -255,13 +254,8 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil
Objects.requireNonNull(algorithmId, "algorithmId");
Objects.requireNonNull(privateKey, "privateKey");
- Supplier> factory = () -> {
- try {
- return CryptoAlgorithms.create(algorithmId, KeyUsage.SIGN, privateKey, spec);
- } catch (IOException e) {
- throw new IllegalStateException("Failed to create SIGN SignatureContext for: " + algorithmId, e);
- }
- };
+ Supplier> factory = () -> session.createContext(algorithmId, KeyUsage.SIGN,
+ privateKey, spec);
return core(factory);
}
@@ -300,13 +294,8 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil
Objects.requireNonNull(algorithmId, "algorithmId");
Objects.requireNonNull(publicKey, "publicKey");
- Supplier> factory = () -> {
- try {
- return CryptoAlgorithms.create(algorithmId, KeyUsage.VERIFY, publicKey, spec);
- } catch (IOException e) {
- throw new IllegalStateException("Failed to create VERIFY SignatureContext for: " + algorithmId, e);
- }
- };
+ Supplier> factory = () -> session.createContext(algorithmId,
+ KeyUsage.VERIFY, publicKey, spec);
return core(factory);
}
@@ -324,8 +313,10 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil
public static final class HybridSelector {
private static final int DEFAULT_MAX_BODY_BYTES = 2 * 1024 * 1024;
+ private final ZeroEchoSession session;
- private HybridSelector() {
+ private HybridSelector(ZeroEchoSession session) {
+ this.session = Objects.requireNonNull(session, "session");
}
/**
@@ -402,7 +393,7 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil
Supplier> factory = () -> {
try {
- return HybridSignatureContexts.sign(profile, classicPrivate, pqcPrivate, maxBodyBytes);
+ return HybridSignatureContexts.sign(session, profile, classicPrivate, pqcPrivate, maxBodyBytes);
} catch (RuntimeException e) { // NOPMD
throw e;
} catch (Exception e) {
@@ -449,7 +440,7 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil
Supplier> factory = () -> {
try {
- return HybridSignatureContexts.verify(profile, classicPublic, pqcPublic, maxBodyBytes);
+ return HybridSignatureContexts.verify(session, profile, classicPublic, pqcPublic, maxBodyBytes);
} catch (RuntimeException e) { // NOPMD
throw e;
} catch (Exception e) {
diff --git a/lib/src/main/java/zeroecho/sdk/builders/TagTrailerDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/TagTrailerDataContentBuilder.java
index b01b570..deaec72 100644
--- a/lib/src/main/java/zeroecho/sdk/builders/TagTrailerDataContentBuilder.java
+++ b/lib/src/main/java/zeroecho/sdk/builders/TagTrailerDataContentBuilder.java
@@ -108,9 +108,9 @@ public final class TagTrailerDataContentBuilder implements DataContentBuilder
* Creates a builder bound to a fixed engine instance.
*
*
- * This constructor is backward compatible but ties the builder to a single-use
- * engine. Prefer {@link #TagTrailerDataContentBuilder(Supplier)} when multiple
- * streams are expected.
+ * This form ties the builder to a single-use engine. Prefer
+ * {@link #TagTrailerDataContentBuilder(Supplier)} when multiple streams are
+ * expected.
*
*
* @param engine preconstructed engine instance; must not be {@code null}
diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/AbstractStreamingSignatureDataBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/AbstractStreamingSignatureDataBuilder.java
deleted file mode 100644
index 27d9026..0000000
--- a/lib/src/main/java/zeroecho/sdk/builders/alg/AbstractStreamingSignatureDataBuilder.java
+++ /dev/null
@@ -1,1302 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2026, Leo Galambos
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * 3. All advertising materials mentioning features or use of this software must
- * display the following acknowledgement:
- * This product includes software developed by the Egothor project.
- *
- * 4. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- ******************************************************************************/
-package zeroecho.sdk.builders.alg;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.util.Base64;
-import java.util.Objects;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-import conflux.CtxInterface;
-import conflux.Key;
-import zeroecho.core.CryptoAlgorithm;
-import zeroecho.core.CryptoAlgorithms;
-import zeroecho.core.context.SignatureContext;
-import zeroecho.core.spec.AlgorithmKeySpec;
-import zeroecho.core.spi.AsymmetricKeyBuilder;
-import zeroecho.core.tag.SignatureVerificationStrategy;
-import zeroecho.sdk.builders.core.DataContentBuilder;
-import zeroecho.sdk.content.api.DataContent;
-import zeroecho.sdk.content.api.PlainContent;
-import zeroecho.sdk.io.SignatureTrailerInputStream;
-
-/**
- * A reusable streaming signature builder that signs or verifies data as it
- * flows through an {@link java.io.InputStream}.
- *
- *
- * This abstract builder composes {@link PlainContent} pipelines that either
- * produce a signature while streaming the original input (sign passthrough),
- * emit the signature itself in various encodings, or verify an expected
- * signature while streaming or emit a boolean result. Algorithms are supplied
- * by {@link CryptoAlgorithms}; concrete subclasses provide algorithm-specific
- * details through protected abstract hooks.
- *
- *
- * What this builder does
- *
- * - Obtains an algorithm instance and keys (direct, imported, or
- * generated).
- * - Creates a {@link SignatureContext} in SIGN or VERIFY mode on demand.
- * - Builds a streaming pipeline that either passes data through or emits a
- * detached artifact.
- * - Lets callers plug in a verification approach (constant-time compare,
- * throw-on-mismatch, flag-in-context, etc.).
- *
- *
- * Typical usage
{@code
- * // Signing while passing the original bytes downstream:
- * PlainContent content = new MyAlgStreamingSignatureDataBuilder()
- * .sign()
- * .withPrivateKey(privateKey)
- * .passThrough()
- * .build(true);
- *
- * // Emitting a hex-encoded detached signature:
- * PlainContent sigOut = new MyAlgStreamingSignatureDataBuilder()
- * .sign()
- * .withPrivateKey(privateKey)
- * .emitHexSignature()
- * .build(true);
- *
- * // Verifying against an expected signature and passing data through:
- * PlainContent verified = new MyAlgStreamingSignatureDataBuilder()
- * .verify()
- * .withPublicKey(publicKey)
- * .expectedSignatureBase64(b64Sig)
- * .passThrough()
- * .build(true);
- *
- * // Verifying and emitting a boolean ("true" or "false"):
- * PlainContent ok = new MyAlgStreamingSignatureDataBuilder()
- * .verify()
- * .emitVerificationBoolean()
- * .withPublicKey(publicKey)
- * .expectedSignature(rawSig)
- * .build(true);
- * }
- *
- *
- * The type parameters represent algorithm-specific key specifications:
- *
- *
- * - {@code KG} - key generation specification type
- * - {@code PUB} - public key import specification type
- * - {@code PRIV} - private key import specification type
- *
- *
- *
- * Subclasses supply algorithm name, key spec classes, default key generation
- * supplier, factories for import specs, and creation of
- * {@link SignatureContext} instances for signing and verification.
- *
- *
- * Thread-safety
- *
- * Builders are mutable and not thread-safe. Create and use an instance on a
- * single thread.
- *
- *
- * @param key generation spec type for the algorithm
- * @param public key import spec type for the algorithm
- * @param private key import spec type for the algorithm
- */
-public abstract class AbstractStreamingSignatureDataBuilder
- implements DataContentBuilder {
- private Mode mode = Mode.SIGN;
-
- private PrivateKey privateKey;
- private PublicKey publicKey;
-
- private boolean genKeyPair;
-
- private byte[] _importPrivatePkcs8;
- private String importPrivateProvider; // optional
-
- private byte[] _importPublicX509;
- private String importPublicProvider; // optional
-
- private byte[] _expectedSignature; // VERIFY: raw
- private Key _expectedSignatureFromCtx; // optional ctx fetch
-
- private Output out = Output.PASSTHROUGH;
-
- private CtxInterface ctx; // optional
- private Key storeSigKey; // optional
- private SignatureVerificationStrategy _strategy; // = null optional
-
- private Consumer sigCallback; // optional
-
- private int _bufferSize = 8192;
-
- private CryptoAlgorithm algorithm; // resolved in resolveKeys()
-
- /**
- * Returns the canonical algorithm name used to resolve an implementation from
- * {@link CryptoAlgorithms}.
- *
- *
- * Examples include "Ed25519" or "SPHINCS+".
- *
- *
- * @return the algorithm name understood by
- * {@link CryptoAlgorithms#require(String)}
- */
- protected abstract String algorithmName();
-
- /**
- * Creates a {@link SignatureContext} in sign mode for the given algorithm and
- * private key.
- *
- *
- * Implementations should instantiate a signing context configured for streaming
- * updates and producing the final signature tag.
- *
- *
- * @param alg the resolved algorithm instance
- * @param key the private key used to generate signatures
- * @return a new signature context in sign mode
- * @throws GeneralSecurityException if the context cannot be created for the
- * provided algorithm or key
- */
- protected abstract SignatureContext newSignContext(CryptoAlgorithm alg, PrivateKey key)
- throws GeneralSecurityException;
-
- /**
- * Creates a {@link SignatureContext} in verify mode for the given algorithm and
- * public key.
- *
- *
- * Implementations should instantiate a verification context configured for
- * streaming updates and validating the final signature tag.
- *
- *
- * @param alg the resolved algorithm instance
- * @param key the public key used to verify signatures
- * @return a new signature context in verify mode
- * @throws GeneralSecurityException if the context cannot be created for the
- * provided algorithm or key
- */
- protected abstract SignatureContext newVerifyContext(CryptoAlgorithm alg, PublicKey key)
- throws GeneralSecurityException;
-
- /**
- * Returns the key generation specification class for the algorithm.
- *
- * @return the class object representing {@code KG}
- */
- protected abstract Class keyGenSpecClass();
-
- /**
- * Returns the public key import specification class for the algorithm.
- *
- * @return the class object representing {@code PUB}
- */
- protected abstract Class publicKeySpecClass();
-
- /**
- * Returns the private key import specification class for the algorithm.
- *
- * @return the class object representing {@code PRIV}
- */
- protected abstract Class privateKeySpecClass();
-
- /**
- * Returns a supplier of default key generation specifications.
- *
- *
- * The supplier must not return null when invoked.
- *
- *
- * @return a non-null supplier of default {@code KG} instances
- */
- protected abstract Supplier defaultKeyGenSpecSupplier();
-
- /**
- * Returns the currently configured key generation specification or null if the
- * default should be used.
- *
- *
- * Subclasses typically provide a public setter to allow users to set a
- * non-default specification.
- *
- *
- * @return the current key generation spec or null to indicate default should be
- * used
- */
- protected abstract KG currentKeyGenSpecOrNull();
-
- /**
- * Builds a public key import specification from X.509-encoded bytes.
- *
- *
- * The {@code providerHint} may be ignored if not applicable to the
- * implementation.
- *
- *
- * @param x509 the X.509 SubjectPublicKeyInfo bytes
- * @param providerHint an optional provider name hint, may be null
- * @return the public key import spec instance
- */
- protected abstract PUB makePublicKeySpec(byte[] x509, String providerHint);
-
- /**
- * Builds a private key import specification from PKCS#8-encoded bytes.
- *
- *
- * The {@code providerHint} may be ignored if not applicable to the
- * implementation.
- *
- *
- * @param pkcs8 the PKCS#8 PrivateKeyInfo bytes
- * @param providerHint an optional provider name hint, may be null
- * @return the private key import spec instance
- */
- protected abstract PRIV makePrivateKeySpec(byte[] pkcs8, String providerHint);
-
- /**
- * Returns the default provider name hint used when importing keys if no
- * explicit provider was set.
- *
- * @return the provider hint or null if there is no preference
- */
- protected abstract String defaultProviderHint();
-
- /**
- * Operating mode for the builder.
- */
- public enum Mode {
- /**
- * Sign mode produces a signature using a private key.
- */
- SIGN,
- /**
- * Verify mode checks an expected signature using a public key.
- */
- VERIFY
- }
-
- /**
- * Declares the output mode for a cryptographic operation.
- *
- *
- * Each constant specifies how the result of an operation such as signing,
- * verification, or transformation should be returned or rendered.
- *
- *
- * Modes
- *
- * - {@link #PASSTHROUGH} - Return the original data without
- * modification.
- * - {@link #SIG_RAW} - Return the raw signature bytes produced by the
- * algorithm.
- * - {@link #SIG_HEX} - Return the signature encoded as a hexadecimal
- * string.
- * - {@link #SIG_BASE64} - Return the signature encoded in Base64.
- * - {@link #VERIFY_BOOL} - Return a boolean result of verification
- * ({@code true} if valid, {@code false} otherwise).
- *
- *
- * Thread-safety
Enum constants are immutable and inherently
- * thread-safe.
- */
- private enum Output {
- /** Return the original data without modification. */
- PASSTHROUGH,
- /** Return raw signature bytes. */
- SIG_RAW,
- /** Return the signature as a hexadecimal string. */
- SIG_HEX,
- /** Return the signature as a Base64-encoded string. */
- SIG_BASE64,
- /** Return a boolean verification result. */
- VERIFY_BOOL
- }
-
- /**
- * Switches the builder to sign mode.
- *
- * @return {@code this} builder for chaining
- */
- public AbstractStreamingSignatureDataBuilder sign() {
- this.mode = Mode.SIGN;
- return this;
- }
-
- /**
- * Switches the builder to verify mode.
- *
- * @return {@code this} builder for chaining
- */
- public AbstractStreamingSignatureDataBuilder verify() {
- this.mode = Mode.VERIFY;
- return this;
- }
-
- /**
- * Configures the output to pass the original data through unchanged.
- *
- *
- * In sign mode this computes the signature but appends it only when using the
- * internal trailer format. In verify mode this verifies as the data is consumed
- * while passing it through.
- *
- *
- * @return {@code this} builder for chaining
- */
- public AbstractStreamingSignatureDataBuilder passThrough() {
- this.out = Output.PASSTHROUGH;
- return this;
- }
-
- /**
- * Configures the output to emit the raw detached signature bytes.
- *
- * @return {@code this} builder for chaining
- */
- public AbstractStreamingSignatureDataBuilder emitRawSignature() {
- this.out = Output.SIG_RAW;
- return this;
- }
-
- /**
- * Configures the output to emit the detached signature as lowercase hexadecimal
- * text.
- *
- * @return {@code this} builder for chaining
- */
- public AbstractStreamingSignatureDataBuilder emitHexSignature() {
- this.out = Output.SIG_HEX;
- return this;
- }
-
- /**
- * Configures the output to emit the detached signature as Base64 text.
- *
- * @return {@code this} builder for chaining
- */
- public AbstractStreamingSignatureDataBuilder emitBase64Signature() {
- this.out = Output.SIG_BASE64;
- return this;
- }
-
- /**
- * Configures the builder to verify and emit a boolean result encoded as ASCII
- * "true" or "false".
- *
- *
- * This method also switches the mode to {@link Mode#VERIFY}.
- *
- *
- * @return {@code this} builder for chaining
- */
- public AbstractStreamingSignatureDataBuilder emitVerificationBoolean() {
- this.mode = Mode.VERIFY;
- this.out = Output.VERIFY_BOOL;
- return this;
- }
-
- /**
- * Sets the internal streaming buffer size used when consuming input.
- *
- * @param bytes buffer size in bytes, must be greater than or equal to 1
- * @return {@code this} builder for chaining
- * @throws IllegalArgumentException if {@code bytes < 1}
- */
- public AbstractStreamingSignatureDataBuilder bufferSize(int bytes) {
- if (bytes < 1) { // NOPMD
- throw new IllegalArgumentException("bufferSize must be >= 1");
- }
- this._bufferSize = bytes;
- return this;
- }
-
- /**
- * Sets the private key to be used in sign mode.
- *
- * @param k the private key, must not be null
- * @return {@code this} builder for chaining
- * @throws NullPointerException if {@code k} is null
- */
- public AbstractStreamingSignatureDataBuilder withPrivateKey(PrivateKey k) {
- this.privateKey = Objects.requireNonNull(k);
- return this;
- }
-
- /**
- * Sets the public key to be used in verify mode.
- *
- * @param k the public key, must not be null
- * @return {@code this} builder for chaining
- * @throws NullPointerException if {@code k} is null
- */
- public AbstractStreamingSignatureDataBuilder withPublicKey(PublicKey k) {
- this.publicKey = Objects.requireNonNull(k);
- return this;
- }
-
- /**
- * Requests generation of a fresh key pair using the algorithm-specific key
- * generation spec.
- *
- *
- * If called, a key pair will be generated during {@link #build(boolean)} using
- * either the current key generation spec or a default one from
- * {@link #defaultKeyGenSpecSupplier()}.
- *
- *
- * @return {@code this} builder for chaining
- */
- public AbstractStreamingSignatureDataBuilder generateKeyPair() {
- this.genKeyPair = true;
- return this;
- }
-
- /**
- * Provides a PKCS#8-encoded private key to import using the default provider
- * hint.
- *
- * @param pkcs8 PKCS#8 PrivateKeyInfo bytes, must not be null
- * @return {@code this} builder for chaining
- * @throws NullPointerException if {@code pkcs8} is null
- */
- public AbstractStreamingSignatureDataBuilder importPrivatePkcs8(byte[] pkcs8) {
- this._importPrivatePkcs8 = Objects.requireNonNull(pkcs8).clone();
- this.importPrivateProvider = null;
- return this;
- }
-
- /**
- * Provides a PKCS#8-encoded private key to import using the given provider
- * name.
- *
- * @param pkcs8 PKCS#8 PrivateKeyInfo bytes, must not be null
- * @param providerName provider name hint to use, may be null
- * @return {@code this} builder for chaining
- * @throws NullPointerException if {@code pkcs8} is null
- */
- public AbstractStreamingSignatureDataBuilder importPrivatePkcs8(byte[] pkcs8, String providerName) {
- this._importPrivatePkcs8 = Objects.requireNonNull(pkcs8).clone();
- this.importPrivateProvider = providerName;
- return this;
- }
-
- /**
- * Provides an X.509-encoded public key to import using the default provider
- * hint.
- *
- * @param x509 X.509 SubjectPublicKeyInfo bytes, must not be null
- * @return {@code this} builder for chaining
- * @throws NullPointerException if {@code x509} is null
- */
- public AbstractStreamingSignatureDataBuilder importPublicX509(byte[] x509) {
- this._importPublicX509 = Objects.requireNonNull(x509).clone();
- this.importPublicProvider = null;
- return this;
- }
-
- /**
- * Provides an X.509-encoded public key to import using the given provider name.
- *
- * @param x509 X.509 SubjectPublicKeyInfo bytes, must not be null
- * @param providerName provider name hint to use, may be null
- * @return {@code this} builder for chaining
- * @throws NullPointerException if {@code x509} is null
- */
- public AbstractStreamingSignatureDataBuilder importPublicX509(byte[] x509, String providerName) {
- this._importPublicX509 = Objects.requireNonNull(x509).clone();
- this.importPublicProvider = providerName;
- return this;
- }
-
- /**
- * Sets the expected signature for verification as raw bytes.
- *
- * @param raw the expected signature bytes, must not be null
- * @return {@code this} builder for chaining
- * @throws NullPointerException if {@code raw} is null
- */
- public AbstractStreamingSignatureDataBuilder expectedSignature(byte[] raw) {
- this._expectedSignature = Objects.requireNonNull(raw).clone();
- return this;
- }
-
- /**
- * Sets the expected signature for verification from a hexadecimal string.
- *
- * @param hex lowercase or uppercase hexadecimal string, must not be null
- * @return {@code this} builder for chaining
- * @throws NullPointerException if {@code hex} is null
- * @throws IllegalArgumentException if {@code hex} is not valid hexadecimal
- */
- public AbstractStreamingSignatureDataBuilder expectedSignatureHex(String hex) {
- this._expectedSignature = java.util.HexFormat.of().parseHex(Objects.requireNonNull(hex));
- return this;
- }
-
- /**
- * Sets the expected signature for verification from a Base64 string.
- *
- * @param b64 Base64-encoded signature text, must not be null
- * @return {@code this} builder for chaining
- * @throws NullPointerException if {@code b64} is null
- * @throws IllegalArgumentException if {@code b64} is not valid Base64
- */
- public AbstractStreamingSignatureDataBuilder expectedSignatureBase64(String b64) {
- this._expectedSignature = Base64.getDecoder().decode(Objects.requireNonNull(b64));
- return this;
- }
-
- /**
- * Configures verification to fetch the expected signature bytes from a context
- * when building the stream.
- *
- * @param key the context key under which the expected signature is stored
- * @return {@code this} builder for chaining
- * @throws NullPointerException if {@code key} is null
- */
- public AbstractStreamingSignatureDataBuilder expectedSignatureFromCtx(Key key) {
- this._expectedSignatureFromCtx = Objects.requireNonNull(key);
- return this;
- }
-
- /**
- * Sets the optional runtime context to read or write auxiliary values such as a
- * generated signature or verification result.
- *
- * @param c the context instance, may be null to disable context integration
- * @return {@code this} builder for chaining
- */
- public AbstractStreamingSignatureDataBuilder context(CtxInterface c) {
- this.ctx = c;
- return this;
- }
-
- /**
- * Configures the context key under which a generated signature will be stored
- * after signing.
- *
- * @param key the context key to store the signature under, may be null to
- * disable storage
- * @return {@code this} builder for chaining
- */
- public AbstractStreamingSignatureDataBuilder storeSignature(Key key) {
- this.storeSigKey = key;
- return this;
- }
-
- /**
- * Registers a callback that receives the generated signature bytes after
- * signing completes.
- *
- * @param cb the callback to invoke with a defensive copy of the signature, may
- * be null
- * @return {@code this} builder for chaining
- */
- public AbstractStreamingSignatureDataBuilder onSignature(Consumer cb) {
- this.sigCallback = cb;
- return this;
- }
-
- /**
- * Sets a custom verification approach to be applied by verify-mode pipelines.
- *
- *
- * The strategy defines how the computed and expected tags are compared and how
- * failures are surfaced. For example, callers may supply
- * {@code getVerificationCore().getThrowOnMismatch()} to raise on mismatch, or a
- * decorated variant that records a boolean flag in a context.
- *
- *
- * Default behavior
- *
- * If this method is not called, verify-mode pipelines use the default core with
- * throw-on-mismatch semantics.
- *
- *
- * @param strategy verification strategy; if {@code null}, the default core is
- * used
- * @return {@code this} builder for chaining
- */
- public AbstractStreamingSignatureDataBuilder withStrategy(SignatureVerificationStrategy strategy) {
- this._strategy = strategy;
- return this;
- }
-
- /**
- * Builds the configured streaming signature pipeline as {@link PlainContent}.
- *
- *
- * This method resolves the algorithm and keys according to the current
- * configuration, then returns a {@link PlainContent} that will perform signing
- * or verification when its stream is consumed.
- *
- *
- *
- * The boolean parameter is ignored and present only to satisfy the
- * {@link DataContentBuilder} interface.
- *
- *
- * {@code
- * PlainContent pipeline = builder
- * .sign()
- * .withPrivateKey(pk)
- * .passThrough()
- * .build(true);
- * }
- *
- * @param ignored not used
- * @return a {@link PlainContent} instance that performs the requested operation
- * on stream consumption
- * @throws IllegalStateException if required keys are missing for the selected
- * mode or if key setup fails
- */
- @Override
- public PlainContent build(boolean ignored) {
- try {
- resolveKeys();
- } catch (GeneralSecurityException e) {
- throw new IllegalStateException(algorithmName() + " key setup failed", e);
- }
- return switch (mode) {
- case SIGN -> {
- if (privateKey == null) {
- throw new IllegalStateException("SIGN mode needs a PrivateKey");
- }
- yield (out == Output.PASSTHROUGH)
- ? new SignPassthrough(algorithm, privateKey, ctx, storeSigKey, sigCallback, _bufferSize)
- : new SignEmit(algorithm, privateKey, out, ctx, storeSigKey, sigCallback, _bufferSize);
- }
- case VERIFY -> {
- if (publicKey == null) {
- throw new IllegalStateException("VERIFY mode needs a PublicKey");
- }
- yield (out == Output.VERIFY_BOOL)
- ? new VerifyEmit(algorithm, publicKey, _expectedSignature, _expectedSignatureFromCtx, ctx,
- _strategy)
- : new VerifyPassthrough(algorithm, publicKey, _expectedSignature, _expectedSignatureFromCtx,
- ctx, _strategy);
- }
- };
- }
-
- private void resolveKeys() throws GeneralSecurityException {
- this.algorithm = CryptoAlgorithms.require(algorithmName());
-
- if (genKeyPair) {
- final Supplier sup = Objects.requireNonNull(defaultKeyGenSpecSupplier(), "defaultKeyGenSpecSupplier");
- final KG spec = (currentKeyGenSpecOrNull() != null) ? currentKeyGenSpecOrNull() : sup.get();
- final AsymmetricKeyBuilder b = algorithm.asymmetricKeyBuilder(keyGenSpecClass());
- final KeyPair kp = b.generateKeyPair(spec);
- this.privateKey = kp.getPrivate();
- this.publicKey = kp.getPublic();
- }
- if (_importPrivatePkcs8 != null) {
- final String prov = (importPrivateProvider != null) ? importPrivateProvider : defaultProviderHint();
- final PRIV privSpec = makePrivateKeySpec(_importPrivatePkcs8, prov);
- final AsymmetricKeyBuilder b = algorithm.asymmetricKeyBuilder(privateKeySpecClass());
- this.privateKey = b.importPrivate(privSpec);
- }
- if (_importPublicX509 != null) {
- final String prov = (importPublicProvider != null) ? importPublicProvider : defaultProviderHint();
- final PUB pubSpec = makePublicKeySpec(_importPublicX509, prov);
- final AsymmetricKeyBuilder b = algorithm.asymmetricKeyBuilder(publicKeySpecClass());
- this.publicKey = b.importPublic(pubSpec);
- }
- }
-
- /**
- * Pass-through content that signs the streamed bytes and emits the final
- * signature.
- *
- *
- * {@code SignPassthrough} attaches to an upstream {@link DataContent}, returns
- * an {@link InputStream} that forwards all bytes unchanged, and computes a
- * digital signature as the stream is consumed. When the stream reaches EOF, the
- * signature is finalized and can be stored in a {@link CtxInterface} and/or
- * delivered to a callback if configured.
- *
- *
- * Behavior
- *
- * - Signing context is created lazily in {@link #getStream()} via
- * {@code newSignContext(alg, key)}.
- * - The returned stream is read-only and must be consumed to EOF to produce a
- * signature.
- * - Failures while storing or delivering the signature are swallowed to avoid
- * disrupting the caller's read loop.
- *
- */
- private final class SignPassthrough implements PlainContent {
- private final CryptoAlgorithm alg;
- private final PrivateKey key;
- private final CtxInterface ctx;
- private final Key storeKey;
- private final Consumer cb;
- private final int bufferSize;
- private volatile DataContent upstream; // NOPMD
-
- /**
- * Creates a new pass-through signer.
- *
- * @param alg the algorithm that provides a {@link SignatureContext};
- * must not be {@code null}
- * @param key the private key used for signing; must not be {@code null}
- * @param ctx optional context used to store the produced signature; may
- * be {@code null}
- * @param storeKey optional key under which the signature is stored in
- * {@code ctx}; may be {@code null}
- * @param cb optional callback invoked with a defensive copy of the
- * signature; may be {@code null}
- * @param bufferSize the internal buffer size used by the trailer stream
- * @throws NullPointerException if {@code alg} or {@code key} is {@code null}
- */
- private SignPassthrough(CryptoAlgorithm alg, PrivateKey key, CtxInterface ctx, Key storeKey,
- Consumer cb, int bufferSize) {
- this.alg = Objects.requireNonNull(alg);
- this.key = Objects.requireNonNull(key);
- this.ctx = ctx;
- this.storeKey = storeKey;
- this.cb = cb;
- this.bufferSize = bufferSize;
- }
-
- /**
- * Sets the upstream content that will be passed through and signed.
- *
- * @param input the upstream data source; must not be {@code null}
- * @throws NullPointerException if {@code input} is {@code null}
- */
- @Override
- public void setInput(DataContent input) {
- this.upstream = Objects.requireNonNull(input);
- }
-
- /**
- * Returns a stream that forwards upstream bytes unmodified and computes a
- * signature.
- *
- *
- * The signature is finalized when the returned stream reaches EOF and is then:
- *
- *
- * - stored into {@code ctx} under {@code storeKey} if both are non-null,
- * and
- * - delivered to {@code cb} if non-null (the byte array passed to the
- * callback is a clone).
- *
- *
- * @return an input stream that signs data while passing it through unchanged
- * @throws IOException if the signing context cannot be initialized or
- * if the underlying upstream stream throws an I/O
- * error
- * @throws NullPointerException if {@link #setInput(DataContent)} was not called
- * before invocation
- */
- @Override
- public InputStream getStream() throws IOException {
- Objects.requireNonNull(upstream, "sign: missing input");
- final SignatureContext sc;
- try {
- sc = newSignContext(alg, key);
- } catch (GeneralSecurityException e) {
- throw new IOException("Failed to init sign context", e);
- }
-
- return new SignatureTrailerInputStream(sc, upstream.getStream(), bufferSize, sig -> {
- if (ctx != null && storeKey != null) {
- try {
- ctx.put(storeKey, sig);
- } catch (RuntimeException ignore) { // NOPMD
- }
- }
- if (cb != null) {
- try {
- cb.accept(sig.clone());
- } catch (RuntimeException ignore) { // NOPMD
- }
- }
- });
- }
- }
-
- /**
- * Eager-signing content that emits only the signature in a chosen encoding.
- *
- *
- * {@code SignEmit} consumes the entire upstream {@link DataContent}, computes a
- * digital signature, optionally stores and/or publishes the signature, and
- * returns an {@link InputStream} over the encoded signature bytes. Unlike a
- * pass-through variant, the payload is fully drained internally and is not
- * forwarded to the caller; the resulting stream contains only the signature
- * material in the requested format.
- *
- *
- * Behavior
- *
- * - Only {@code SIG_*} output modes are accepted.
- * - Signing context is created in {@link #getStream()} via
- * {@code newSignContext(alg, key)}.
- * - Upstream is read to EOF inside {@link #getStream()} to finalize the
- * signature.
- * - On success, the signature is optionally stored and/or passed to a
- * callback; side-effect failures are swallowed.
- *
- */
- private final class SignEmit implements PlainContent {
- private final CryptoAlgorithm alg;
- private final PrivateKey key;
- private final Output out;
- private final CtxInterface ctx;
- private final Key storeKey;
- private final Consumer cb;
- private final int bufferSize;
- private volatile DataContent upstream; // NOPMD
-
- /**
- * Creates a new signer that emits the signature in the requested format.
- *
- * @param alg the algorithm used to create a {@link SignatureContext};
- * must not be {@code null}
- * @param key the private key used for signing; must not be {@code null}
- * @param out the desired signature output format; must be one of
- * {@link Output#SIG_RAW}, {@link Output#SIG_HEX}, or
- * {@link Output#SIG_BASE64}
- * @param ctx optional context used to store the produced signature; may
- * be {@code null}
- * @param storeKey optional key under which the signature is stored in
- * {@code ctx}; may be {@code null}
- * @param cb optional callback invoked with a defensive copy of the
- * signature; may be {@code null}
- * @param bufferSize the internal buffer size used by the trailer stream
- * @throws IllegalArgumentException if {@code out} is not a {@code SIG_*}
- * variant
- * @throws NullPointerException if {@code alg} or {@code key} is
- * {@code null}
- */
- private SignEmit(CryptoAlgorithm alg, PrivateKey key, Output out, CtxInterface ctx, Key storeKey,
- Consumer cb, int bufferSize) {
- if (out != Output.SIG_RAW && out != Output.SIG_HEX && out != Output.SIG_BASE64) {
- throw new IllegalArgumentException("SignEmit requires SIG_* output");
- }
- this.alg = Objects.requireNonNull(alg);
- this.key = Objects.requireNonNull(key);
- this.out = out;
- this.ctx = ctx;
- this.storeKey = storeKey;
- this.cb = cb;
- this.bufferSize = bufferSize;
- }
-
- /**
- * Sets the upstream content to be read and signed.
- *
- * @param input the upstream data source; must not be {@code null}
- * @throws NullPointerException if {@code input} is {@code null}
- */
- @Override
- public void setInput(DataContent input) {
- this.upstream = Objects.requireNonNull(input);
- }
-
- /**
- * Drains the upstream to compute the signature and returns a stream over the
- * signature bytes.
- *
- *
- * The method creates a {@link SignatureContext}, reads the entire upstream
- * stream to EOF in order to finalize the signature, and then returns an
- * {@link InputStream} over the encoded signature according to {@link #out}:
- *
- *
- * - {@link Output#SIG_RAW} - raw signature bytes,
- * - {@link Output#SIG_HEX} - hexadecimal string encoded as UTF-8 bytes,
- * - {@link Output#SIG_BASE64} - Base64-encoded bytes.
- *
- *
- *
- * If provided, the signature is stored in {@code ctx} under {@code storeKey}
- * and passed to {@code cb}. Both operations use a defensive copy and swallow
- * runtime exceptions to avoid interrupting the primary flow.
- *
- *
- * @return an input stream that yields only the encoded signature bytes
- * @throws IOException if the signing context cannot be initialized,
- * the upstream cannot be read, or no signature
- * trailer is produced
- * @throws NullPointerException if {@link #setInput(DataContent)} was not
- * invoked prior to this call
- */
- @Override
- public InputStream getStream() throws IOException {
- Objects.requireNonNull(upstream, "sign: missing input");
-
- final SignatureContext sc;
- try {
- sc = newSignContext(alg, key);
- } catch (GeneralSecurityException e) {
- throw new IOException("Failed to init sign context", e);
- }
-
- final byte[][] sigHolder = new byte[1][];
-
- try (SignatureTrailerInputStream in = new SignatureTrailerInputStream(sc, upstream.getStream(), bufferSize,
- new Consumer<>() {
- @Override
- public void accept(byte[] sig) {
- sigHolder[0] = (sig == null ? null : sig.clone());
- }
- })) {
- in.transferTo(OutputStream.nullOutputStream());
- } catch (IOException ioe) {
- try {
- sc.close();
- } catch (RuntimeException ignore) { // NOPMD
- }
- throw ioe;
- }
-
- final byte[] sig = sigHolder[0];
- if (sig == null) {
- throw new IOException("Missing signature trailer");
- }
-
- if (ctx != null && storeKey != null) {
- try {
- ctx.put(storeKey, sig.clone());
- } catch (RuntimeException ignore) { // NOPMD
- }
- }
- if (cb != null) {
- try {
- cb.accept(sig.clone());
- } catch (RuntimeException ignore) { // NOPMD
- }
- }
-
- byte[] outBytes = switch (out) {
- case SIG_RAW -> sig;
- case SIG_HEX -> java.util.HexFormat.of().formatHex(sig).getBytes(StandardCharsets.UTF_8);
- case SIG_BASE64 -> Base64.getEncoder().encode(sig);
- default -> throw new IllegalStateException("Unexpected output: " + out);
- };
- return new ByteArrayInputStream(outBytes);
- }
- }
-
- /**
- * Pass-through verifier that forwards bytes unchanged while verifying a
- * signature at EOF.
- *
- *
- * {@code VerifyPassthrough} attaches to an upstream {@link DataContent},
- * returns an {@link InputStream} that yields the original payload, and performs
- * signature verification as the stream is consumed. The expected signature is
- * provided directly or fetched from a {@link CtxInterface}. When the stream
- * reaches EOF, the configured verification strategy determines whether a
- * mismatch raises an error or is handled differently (for example, by flagging
- * in a context if the supplied strategy implements that).
- *
- *
- * Behavior
- *
- * - Expected signature is taken from the {@code expected} field or from
- * {@code ctx.get(expectedKey)}.
- * - The returned stream must be fully drained or closed to finalize
- * verification.
- *
- */
- private final class VerifyPassthrough implements PlainContent {
- private final CryptoAlgorithm alg;
- private final PublicKey key;
- private final byte[] expected;
- private final Key expectedKey;
- private final CtxInterface ctx;
- private final SignatureVerificationStrategy strategy;
- private volatile DataContent upstream; // NOPMD
-
- /**
- * Creates a pass-through verifier that reads from upstream and verifies at EOF.
- *
- * @param alg algorithm used to obtain a {@link SignatureContext}; must
- * not be {@code null}
- * @param key public key used for verification; must not be {@code null}
- * @param expected expected signature bytes (defensively copied), or
- * {@code null} to fetch from {@code ctx}
- * @param expectedKey key in {@code ctx} under which the expected signature may
- * be stored; may be {@code null}
- * @param ctx optional context used to fetch the expected signature; may
- * be {@code null}
- * @param strategy verification approach; if {@code null}, a default
- * throw-on-mismatch strategy is used
- * @throws NullPointerException if {@code alg} or {@code key} is {@code null}
- */
- private VerifyPassthrough(CryptoAlgorithm alg, PublicKey key, byte[] expected, Key expectedKey,
- CtxInterface ctx, SignatureVerificationStrategy strategy) {
- this.alg = Objects.requireNonNull(alg);
- this.key = Objects.requireNonNull(key);
- this.expected = (expected == null ? null : expected.clone());
- this.expectedKey = expectedKey;
- this.ctx = ctx;
- this.strategy = strategy;
- }
-
- /**
- * Sets the upstream content that will be passed through and verified.
- *
- * @param input upstream data source; must not be {@code null}
- * @throws NullPointerException if {@code input} is {@code null}
- */
- @Override
- public void setInput(DataContent input) {
- this.upstream = Objects.requireNonNull(input);
- }
-
- /**
- * Returns a stream that forwards upstream bytes and performs signature
- * verification.
- *
- *
- * The method creates a {@link SignatureContext}, configures the expected
- * signature and verification policy, and wraps the upstream stream. The
- * returned stream must be consumed to EOF (or closed) to finalize verification
- * and, if configured, to store/emit the result.
- *
- *
- * @return an input stream that yields the original bytes while verifying at EOF
- * @throws IOException if the verify context cannot be initialized or
- * the wrapping fails
- * @throws IllegalStateException if no expected signature is available via
- * constructor or context
- * @throws NullPointerException if {@link #setInput(DataContent)} was not
- * invoked prior to this call
- */
- @Override
- public InputStream getStream() throws IOException {
- Objects.requireNonNull(upstream, "verify: missing input");
-
- byte[] exp = expected;
- if (exp == null && ctx != null && expectedKey != null) {
- exp = ctx.get(expectedKey);
- }
- if (exp == null) {
- throw new IllegalStateException("VERIFY requires expectedSignature (or ctx+key)");
- }
-
- final SignatureContext sc; // NOPMD
- try {
- sc = newVerifyContext(alg, key);
- } catch (GeneralSecurityException e) {
- throw new IOException("Failed to init verify context", e);
- }
-
- sc.setExpectedTag(exp);
- if (strategy == null) {
- sc.setVerificationApproach(sc.getVerificationCore().getThrowOnMismatch());
- } else {
- sc.setVerificationApproach(strategy);
- }
-
- try {
- return sc.wrap(upstream.getStream());
- } catch (IOException initFail) {
- try {
- sc.close();
- } catch (RuntimeException ignore) { // NOPMD
- }
- throw initFail;
- }
- }
- }
-
- /**
- * Streaming content wrapper that verifies a digital signature and emits the
- * result as a boolean value.
- *
- *
- * {@code VerifyEmit} consumes an upstream {@link DataContent}, initializes a
- * {@link SignatureContext} with the supplied public key and expected signature,
- * and verifies the signature while streaming the input. The verification
- * outcome ({@code true} or {@code false}) is then returned as a one-shot
- * {@link java.io.InputStream} containing the UTF-8 encoded string
- * {@code "true"} or {@code "false"}.
- *
- *
- * Notes
- *
- * - The expected signature may be provided directly or looked up from a
- * {@link CtxInterface} via {@code expectedKey}.
- * - Outcome computation follows the configured verification strategy; I/O or
- * verification failures result in {@code "false"}.
- *
- */
- private final class VerifyEmit implements PlainContent {
- private final CryptoAlgorithm alg;
- private final PublicKey key;
- private final byte[] expected;
- private final Key expectedKey;
- private final CtxInterface ctx;
- private final SignatureVerificationStrategy strategy;
- private volatile DataContent upstream; // NOPMD
-
- /**
- * Creates a streaming verifier that consumes upstream data and emits a boolean
- * result.
- *
- * @param alg algorithm used to create a {@link SignatureContext}; must
- * not be {@code null}
- * @param key public key used for verification; must not be {@code null}
- * @param expected expected signature bytes; may be {@code null} when
- * {@code ctx} and {@code expectedKey} are provided
- * @param expectedKey context key from which to read the expected signature when
- * {@code expected} is {@code null}; may be {@code null}
- * @param ctx optional context used to read the expected signature; may
- * be {@code null}
- * @param strategy verification approach; if {@code null}, a default
- * throw-on-mismatch strategy is used
- * @throws NullPointerException if {@code alg} or {@code key} is {@code null}
- */
- private VerifyEmit(CryptoAlgorithm alg, PublicKey key, byte[] expected, Key expectedKey,
- CtxInterface ctx, SignatureVerificationStrategy strategy) {
- this.alg = Objects.requireNonNull(alg);
- this.key = Objects.requireNonNull(key);
- this.expected = (expected == null ? null : expected.clone());
- this.expectedKey = expectedKey;
- this.ctx = ctx;
- this.strategy = strategy;
- }
-
- /**
- * Sets the upstream content that will be consumed and verified.
- *
- *
- * This method must be called exactly once before {@link #getStream()}. The
- * provided {@link DataContent} is stored and later used to obtain the readable
- * stream that is fed into the verification context.
- *
- *
- * @param input upstream data source; must not be {@code null}
- * @throws NullPointerException if {@code input} is {@code null}
- */
- @Override
- public void setInput(DataContent input) {
- this.upstream = Objects.requireNonNull(input);
- }
-
- /**
- * Returns a one-shot stream that yields the UTF-8 text {@code "true"} or
- * {@code "false"} after verifying the upstream content against the expected
- * signature.
- *
- *
- * On entry, this method initializes a {@link SignatureContext}, configures the
- * expected tag and a strict verification policy, and then drains the upstream
- * stream. Any I/O failures or verification mismatches result in the boolean
- * outcome {@code false}; successful verification results in {@code true}. The
- * outcome is optionally stored into {@code ctx} under {@code storeOk} and
- * passed to the callback {@code cb}.
- *
- *
- *
- * Resource management is handled via try-with-resources: the verification
- * context is always closed. If closing the context fails, the method reports
- * {@code false} rather than propagating the close exception, allowing callers
- * to reliably consume the result.
- *
- *
- * Usage
{@code
- * verifyEmit.setInput(data);
- * try (InputStream result = verifyEmit.getStream()) {
- * boolean ok = Boolean.parseBoolean(new String(result.readAllBytes(), StandardCharsets.UTF_8));
- * // use ok
- * }
- * }
- *
- * @return a new input stream that produces {@code "true"} or {@code "false"} in
- * UTF-8
- * @throws IOException if the verify context cannot be initialized or
- * the upstream stream cannot be obtained
- * @throws IllegalStateException if no expected signature is available from the
- * constructor arguments or the context
- * @throws NullPointerException if {@link #setInput(DataContent)} was not
- * called before invocation
- */
- @Override
- public InputStream getStream() throws IOException {
- Objects.requireNonNull(upstream, "verify: missing input");
-
- byte[] exp = expected;
- if (exp == null && ctx != null && expectedKey != null) {
- exp = ctx.get(expectedKey);
- }
- if (exp == null) {
- throw new IllegalStateException("VERIFY requires expectedSignature (or ctx+key)");
- }
-
- final SignatureContext sc;
- try {
- sc = newVerifyContext(alg, key);
- } catch (GeneralSecurityException e) {
- throw new IOException("Failed to init verify context", e);
- }
-
- sc.setExpectedTag(exp);
- if (strategy == null) {
- sc.setVerificationApproach(sc.getVerificationCore().getThrowOnMismatch());
- } else {
- sc.setVerificationApproach(strategy);
- }
-
- try (sc) { // closes sc; if it throws, that exception propagates
- try (InputStream in = sc.wrap(upstream.getStream())) {
- in.transferTo(OutputStream.nullOutputStream());
- return new ByteArrayInputStream("true".getBytes(StandardCharsets.UTF_8));
- } catch (IOException fail) {
- // wrap/read/transfer/close(in) failures land here and are swallowed -> ok =
- // false
- return new ByteArrayInputStream("false".getBytes(StandardCharsets.UTF_8));
- }
- }
- }
- }
-}
diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/AesDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/AesDataContentBuilder.java
index a0e7e01..ae57445 100644
--- a/lib/src/main/java/zeroecho/sdk/builders/alg/AesDataContentBuilder.java
+++ b/lib/src/main/java/zeroecho/sdk/builders/alg/AesDataContentBuilder.java
@@ -33,6 +33,8 @@
******************************************************************************/
package zeroecho.sdk.builders.alg;
+import zeroecho.sdk.ZeroEchoSession;
+
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
@@ -44,7 +46,6 @@ import javax.crypto.SecretKey;
import conflux.Ctx;
import conflux.CtxInterface;
import zeroecho.core.ConfluxKeys;
-import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.SymmetricHeaderCodec;
@@ -54,7 +55,6 @@ import zeroecho.core.alg.aes.AesKeyImportSpec;
import zeroecho.core.alg.aes.AesSpec;
import zeroecho.core.context.EncryptionContext;
import zeroecho.core.spi.ContextAware;
-import zeroecho.core.spi.SymmetricKeyBuilder;
import zeroecho.sdk.builders.core.DataContentBuilder;
import zeroecho.sdk.content.api.DataContent;
import zeroecho.sdk.content.api.EncryptedContent;
@@ -106,6 +106,8 @@ import zeroecho.sdk.content.api.PlainContent;
* @since 1.0
*/
public final class AesDataContentBuilder implements DataContentBuilder {
+ private static final String ALGORITHM_ID = "AES";
+ private final ZeroEchoSession session;
private SecretKey secretKey;
private AesKeyGenSpec genSpec;
private AesKeyImportSpec importSpec;
@@ -128,11 +130,12 @@ public final class AesDataContentBuilder implements DataContentBuilder 0) {
if (ctx == null) {
ctx = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime());
}
- ctx.put(ConfluxKeys.iv("AES"), iv);
+ ctx.put(ConfluxKeys.iv(ALGORITHM_ID), iv);
}
return encrypt ? new EncryptContent(key, aesSpec, ctx) : new DecryptContent(key, aesSpec, ctx);
@@ -387,18 +390,14 @@ public final class AesDataContentBuilder implements DataContentBuilder b = algo.symmetricKeyBuilder(AesKeyGenSpec.class);
- generatedKey = b.generateSecret(genSpec);
+ generatedKey = session.keyBuilders().symmetric().generate(ALGORITHM_ID, genSpec);
return generatedKey;
}
if (importSpec != null) {
- SymmetricKeyBuilder b = algo.symmetricKeyBuilder(AesKeyImportSpec.class);
- return b.importSecret(importSpec);
+ return session.keyBuilders().symmetric().importKey(ALGORITHM_ID, importSpec);
}
- SymmetricKeyBuilder b = algo.symmetricKeyBuilder(AesKeyGenSpec.class);
- generatedKey = b.generateSecret(AesKeyGenSpec.aes256());
+ generatedKey = session.keyBuilders().symmetric().generate(ALGORITHM_ID, AesKeyGenSpec.aes256());
return generatedKey;
} catch (GeneralSecurityException e) {
throw new IllegalStateException("AES key construction failed", e);
@@ -446,7 +445,7 @@ public final class AesDataContentBuilder implements DataContentBuilder
*/
- private static final class EncryptContent implements EncryptedContent {
+ private final class EncryptContent implements EncryptedContent {
private final SecretKey key;
private final AesSpec spec;
private final CtxInterface ctx; // may be null
@@ -523,7 +522,7 @@ public final class AesDataContentBuilder implements DataContentBuilderThread-safety Instances are not thread-safe and are intended for
* single-use pipelines.
*/
- private static final class DecryptContent implements PlainContent {
+ private final class DecryptContent implements PlainContent {
private final SecretKey key;
private final AesSpec spec;
private final CtxInterface ctx; // may be null
@@ -609,7 +608,7 @@ public final class AesDataContentBuilder implements DataContentBuilder
* The method creates a decryption
* {@link zeroecho.core.context.EncryptionContext} via
- * {@link CryptoAlgorithms#create(String, KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)},
+ * {@link zeroecho.sdk.ZeroEchoSession#createContext(String, KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)},
* forwards the optional {@link CtxInterface} when the context is
* {@code ContextAware}, and returns the stream produced by {@code attach}. The
* returned stream is independent of the temporary context and remains usable
@@ -628,7 +627,7 @@ public final class AesDataContentBuilder implements DataContentBuilder {
+ private final ZeroEchoSession session;
private SecretKey secretKey;
private ChaChaKeyGenSpec genSpec;
@@ -174,11 +174,12 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder b = algo.symmetricKeyBuilder(ChaChaKeyGenSpec.class);
- return b.generateSecret(genSpec);
+ return session.keyBuilders().symmetric().generate(algId, genSpec);
}
if (importSpec != null) {
- SymmetricKeyBuilder b = algo.symmetricKeyBuilder(ChaChaKeyImportSpec.class);
- return b.importSecret(importSpec);
+ return session.keyBuilders().symmetric().importKey(algId, importSpec);
}
- SymmetricKeyBuilder b = algo.symmetricKeyBuilder(ChaChaKeyGenSpec.class);
- return b.generateSecret(ChaChaKeyGenSpec.chacha256());
+ return session.keyBuilders().symmetric().generate(algId, ChaChaKeyGenSpec.chacha256());
} catch (GeneralSecurityException e) {
throw new IllegalStateException("ChaCha key construction failed for " + algId, e);
}
@@ -567,12 +564,13 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder
* The actual cipher work is delegated to an
* {@link zeroecho.core.context.EncryptionContext} created through
- * {@link zeroecho.core.CryptoAlgorithms#create(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}.
+ * {@link zeroecho.sdk.ZeroEchoSession#createContext(String,
+ * zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}.
* If the created context implements {@code ContextAware}, the configured
* context is injected before the stream is attached.
*
*/
- private static final class EncryptContent implements EncryptedContent {
+ private final class EncryptContent implements EncryptedContent {
private final String algId;
private final SecretKey key;
private final S spec;
@@ -615,7 +613,7 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder
* The actual cipher work is delegated to an
* {@link zeroecho.core.context.EncryptionContext} created through
- * {@link zeroecho.core.CryptoAlgorithms#create(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}.
+ * {@link zeroecho.sdk.ZeroEchoSession#createContext(String,
+ * zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}.
* If the created context implements {@code ContextAware}, the configured
* context is injected before the stream is attached.
*
*/
- private static final class DecryptContent implements PlainContent {
+ private final class DecryptContent implements PlainContent {
private final String algId;
private final SecretKey key;
private final S spec;
@@ -686,7 +685,7 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder {
+ private final ZeroEchoSession session;
/**
* OutputMode selects how the digest-computing pipeline presents its result to
* callers.
@@ -184,7 +186,8 @@ public final class DigestDataContentBuilder implements DataContentBuilder callback; // optional
private int bufferSize = 8192; // internal I/O buffer for tail-stripper
- private DigestDataContentBuilder() {
+ private DigestDataContentBuilder(ZeroEchoSession session) {
+ this.session = Objects.requireNonNull(session, "session must not be null");
}
/**
@@ -200,8 +203,8 @@ public final class DigestDataContentBuilder implements DataContentBuilder storeKey;
@@ -444,7 +447,8 @@ public final class DigestDataContentBuilder implements DataContentBuilder storeKey;
@@ -514,7 +518,8 @@ public final class DigestDataContentBuilder implements DataContentBuilderOverview This builder specializes
- * {@link AbstractStreamingSignatureDataBuilder} for ECDSA and lets callers
- * choose a named curve before constructing a signing or verification pipeline.
- * The actual signing and verification work is performed by
- * {@link SignatureContext} instances created via JCA-backed factories.
- *
- * Typical usage
{@code
- * // Sign while passing the original bytes through:
- * PlainContent signed = EcdsaDataContentBuilder.builder()
- * .withCurveP256()
- * .sign()
- * .withPrivateKey(privateKey)
- * .passThrough()
- * .build(true);
- *
- * // Emit a detached Base64 signature:
- * PlainContent sigOut = EcdsaDataContentBuilder.builder()
- * .withCurve(EcdsaCurveSpec.P384)
- * .sign()
- * .withPrivateKey(privateKey)
- * .emitBase64Signature()
- * .build(true);
- *
- * // Verify while passing the original bytes through:
- * PlainContent verified = EcdsaDataContentBuilder.builder()
- * .withCurveP256()
- * .verify()
- * .withPublicKey(publicKey)
- * .expectedSignature(rawSig)
- * .passThrough()
- * .build(true);
- * }
- *
- * Curve selection
If no curve is selected explicitly, {@code P256} is
- * used. Convenience methods are provided for P-256, P-384, and P-512
- * (implementation-specific name used by {@link EcdsaCurveSpec}).
- *
- * Thread-safety
Instances are mutable and not thread-safe. Configure
- * and use each builder instance from a single thread.
- *
- * @see AbstractStreamingSignatureDataBuilder
- * @see EcdsaCurveSpec
- * @see EcdsaPublicKeySpec
- * @see EcdsaPrivateKeySpec
- * @see SignatureContext
- * @see GenericJcaSignatureContext
- */
-public final class EcdsaDataContentBuilder
- extends AbstractStreamingSignatureDataBuilder {
-
- private final static EcdsaCurveSpec DEFAULT = EcdsaCurveSpec.P256;
-
- private EcdsaCurveSpec selected = DEFAULT;
-
- /**
- * Creates a new builder instance with the default curve selection.
- *
- * Example
{@code
- * EcdsaDataContentBuilder b = EcdsaDataContentBuilder.builder();
- * }
- *
- * @return a new {@code EcdsaDataContentBuilder}
- */
- public static EcdsaDataContentBuilder builder() {
- return new EcdsaDataContentBuilder();
- }
-
- /**
- * Selects the curve to be used for key generation and signature processing.
- *
- * @param spec the ECDSA curve specification; must not be null
- * @return {@code this} builder for chaining
- * @throws NullPointerException if {@code spec} is null
- */
- public EcdsaDataContentBuilder withCurve(final EcdsaCurveSpec spec) {
- Objects.requireNonNull(spec, "EcdsaCurveSpec cannot be null");
- this.selected = spec;
- return this;
- }
-
- /**
- * Selects the P-256 curve.
- *
- * @return {@code this} builder for chaining
- */
- public EcdsaDataContentBuilder withCurveP256() {
- this.selected = EcdsaCurveSpec.P256;
- return this;
- }
-
- /**
- * Selects the P-384 curve.
- *
- * @return {@code this} builder for chaining
- */
- public EcdsaDataContentBuilder withCurveP384() {
- this.selected = EcdsaCurveSpec.P384;
- return this;
- }
-
- /**
- * Selects the P-512 curve as defined by {@link EcdsaCurveSpec}.
- *
- * @return {@code this} builder for chaining
- */
- public EcdsaDataContentBuilder withCurveP512() {
- this.selected = EcdsaCurveSpec.P512;
- return this;
- }
-
- /**
- * Returns the algorithm name used to resolve an implementation from
- * {@link CryptoAlgorithms}.
- *
- * @return the string {@code "ECDSA"}
- */
- @Override
- protected String algorithmName() {
- return "ECDSA";
- }
-
- private EcdsaCurveSpec activeSpec() {
- return selected;
- }
-
- /**
- * Creates a signing {@link SignatureContext} for the active curve using the
- * provided algorithm and key.
- *
- *
- * The returned context is configured with a JCA signature factory derived from
- * the curve's {@link EcdsaCurveSpec#jcaFactory()} and a fixed-length resolver
- * based on {@link EcdsaCurveSpec#signFixedLength()}.
- *
- *
- * @param alg the resolved crypto algorithm
- * @param key the private key to use for signing
- * @return a new signature context in sign mode
- * @throws GeneralSecurityException if the context cannot be created
- */
- @Override
- protected SignatureContext newSignContext(final CryptoAlgorithm alg, final PrivateKey key)
- throws GeneralSecurityException {
- EcdsaCurveSpec s = activeSpec();
- return new GenericJcaSignatureContext(alg, key, GenericJcaSignatureContext.jcaFactory(s.jcaFactory(), null),
- GenericJcaSignatureContext.SignLengthResolver.fixed(s.signFixedLength()));
- }
-
- /**
- * Creates a verification {@link SignatureContext} for the active curve using
- * the provided algorithm and key.
- *
- *
- * The returned context is configured with a JCA signature factory derived from
- * the curve's {@link EcdsaCurveSpec#jcaFactory()} and a fixed-length resolver
- * based on {@link EcdsaCurveSpec#signFixedLength()}.
- *
- *
- * @param alg the resolved crypto algorithm
- * @param key the public key to use for verification
- * @return a new signature context in verify mode
- * @throws GeneralSecurityException if the context cannot be created
- */
- @Override
- protected SignatureContext newVerifyContext(final CryptoAlgorithm alg, final PublicKey key)
- throws GeneralSecurityException {
- EcdsaCurveSpec s = activeSpec();
- return new GenericJcaSignatureContext(alg, key, GenericJcaSignatureContext.jcaFactory(s.jcaFactory(), null),
- GenericJcaSignatureContext.VerifyLengthResolver.fixed(s.signFixedLength()));
- }
-
- /**
- * Returns the class object of the key generation specification used by this
- * builder.
- *
- * @return {@code EcdsaCurveSpec.class}
- */
- @Override
- protected Class keyGenSpecClass() {
- return EcdsaCurveSpec.class;
- }
-
- /**
- * Returns the class object of the public key import specification used by this
- * builder.
- *
- * @return {@code EcdsaPublicKeySpec.class}
- */
- @Override
- protected Class publicKeySpecClass() {
- return EcdsaPublicKeySpec.class;
- }
-
- /**
- * Returns the class object of the private key import specification used by this
- * builder.
- *
- * @return {@code EcdsaPrivateKeySpec.class}
- */
- @Override
- protected Class privateKeySpecClass() {
- return EcdsaPrivateKeySpec.class;
- }
-
- /**
- * Supplies the default key generation specification for this builder.
- *
- * @return a supplier that returns {@link EcdsaCurveSpec#P256}
- */
- @Override
- protected Supplier defaultKeyGenSpecSupplier() {
- return () -> DEFAULT;
- }
-
- /**
- * Returns the currently selected key generation specification, or null to
- * indicate that the default should be used.
- *
- * @return the active {@link EcdsaCurveSpec} or {@code null}
- */
- @Override
- protected EcdsaCurveSpec currentKeyGenSpecOrNull() {
- return selected;
- }
-
- /**
- * Creates a public key import specification from X.509-encoded bytes.
- *
- * @param x509 the SubjectPublicKeyInfo bytes
- * @param provider an optional provider name hint, ignored by this
- * implementation
- * @return a new {@link EcdsaPublicKeySpec} wrapping the provided bytes
- */
- @Override
- protected EcdsaPublicKeySpec makePublicKeySpec(final byte[] x509, final String provider) {
- return new EcdsaPublicKeySpec(x509);
- }
-
- /**
- * Creates a private key import specification from PKCS#8-encoded bytes.
- *
- * @param pkcs8 the PrivateKeyInfo bytes
- * @param provider an optional provider name hint, ignored by this
- * implementation
- * @return a new {@link EcdsaPrivateKeySpec} wrapping the provided bytes
- */
- @Override
- protected EcdsaPrivateKeySpec makePrivateKeySpec(final byte[] pkcs8, final String provider) {
- return new EcdsaPrivateKeySpec(pkcs8);
- }
-
- /**
- * Returns the default provider hint to use when importing keys if none is
- * explicitly set.
- *
- * @return {@code null} to indicate no preference
- */
- @Override
- protected String defaultProviderHint() {
- return null;
- }
-}
diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/Ed25519DataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/Ed25519DataContentBuilder.java
deleted file mode 100644
index ea5b302..0000000
--- a/lib/src/main/java/zeroecho/sdk/builders/alg/Ed25519DataContentBuilder.java
+++ /dev/null
@@ -1,253 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2026, Leo Galambos
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * 3. All advertising materials mentioning features or use of this software must
- * display the following acknowledgement:
- * This product includes software developed by the Egothor project.
- *
- * 4. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- ******************************************************************************/
-package zeroecho.sdk.builders.alg;
-
-import java.security.GeneralSecurityException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.util.function.Supplier;
-
-import zeroecho.core.CryptoAlgorithm;
-import zeroecho.core.alg.ed25519.Ed25519KeyGenSpec;
-import zeroecho.core.alg.ed25519.Ed25519PrivateKeySpec;
-import zeroecho.core.alg.ed25519.Ed25519PublicKeySpec;
-import zeroecho.core.alg.ed25519.Ed25519SignatureContext;
-import zeroecho.core.context.SignatureContext;
-
-/**
- * Ed25519DataContentBuilder builds streaming Ed25519 signature pipelines that
- * sign or verify as an InputStream is consumed.
- *
- * Overview
This builder specializes
- * {@link AbstractStreamingSignatureDataBuilder} for the Ed25519 algorithm. It
- * constructs {@link zeroecho.sdk.content.api.PlainContent} that either passes
- * the original bytes through while computing or checking a detached signature,
- * or emits the signature or verification result directly, depending on the
- * configuration provided by the fluent API defined in the superclass.
- *
- * Typical usage
{@code
- * // Sign while passing the original bytes through:
- * PlainContent signed = Ed25519DataContentBuilder.builder()
- * .sign()
- * .withPrivateKey(privateKey)
- * .passThrough()
- * .build(true);
- *
- * // Emit a Base64 detached signature:
- * PlainContent sigOut = Ed25519DataContentBuilder.builder()
- * .sign()
- * .withPrivateKey(privateKey)
- * .emitBase64Signature()
- * .build(true);
- *
- * // Verify against an expected signature while passing data through:
- * PlainContent verified = Ed25519DataContentBuilder.builder()
- * .verify()
- * .withPublicKey(publicKey)
- * .expectedSignature(rawSig)
- * .passThrough()
- * .build(true);
- * }
- *
- * Key handling
Ed25519 has no tunable parameters for key generation in
- * this builder. A default generation supplier is provided, and imports from
- * X.509 (public) and PKCS#8 (private) encodings are supported.
- *
- * Thread-safety
Instances are mutable and not thread-safe. Configure
- * and use each builder instance from a single thread.
- *
- * @see AbstractStreamingSignatureDataBuilder
- * @see zeroecho.core.CryptoAlgorithms
- * @see zeroecho.core.context.SignatureContext
- */
-public final class Ed25519DataContentBuilder
- extends AbstractStreamingSignatureDataBuilder {
-
- private static final Supplier DEFAULT_GEN = Ed25519KeyGenSpec::defaultSpec;
-
- /**
- * Creates a new builder instance for constructing Ed25519 streaming signature
- * pipelines.
- *
- * Example
{@code
- * Ed25519DataContentBuilder b = Ed25519DataContentBuilder.builder();
- * }
- *
- * @return a new {@code Ed25519DataContentBuilder}
- */
- public static Ed25519DataContentBuilder builder() {
- return new Ed25519DataContentBuilder();
- }
-
- /**
- * Returns the canonical algorithm name used to resolve an implementation from
- * {@link zeroecho.core.CryptoAlgorithms}.
- *
- * @return the string {@code "Ed25519"}
- */
- @Override
- protected String algorithmName() {
- return "Ed25519";
- }
-
- /**
- * Creates a signing {@link SignatureContext} for Ed25519 using the provided
- * algorithm instance and private key.
- *
- * @param alg the resolved algorithm instance used to create the context
- * @param key the private key for signing
- * @return a new {@link SignatureContext} configured for Ed25519 signing
- * @throws GeneralSecurityException if the context cannot be created for the
- * given key or algorithm
- */
- @Override
- protected SignatureContext newSignContext(final CryptoAlgorithm alg, final PrivateKey key)
- throws GeneralSecurityException {
- return new Ed25519SignatureContext(alg, key);
- }
-
- /**
- * Creates a verification {@link SignatureContext} for Ed25519 using the
- * provided algorithm instance and public key.
- *
- * @param alg the resolved algorithm instance used to create the context
- * @param key the public key for verification
- * @return a new {@link SignatureContext} configured for Ed25519 verification
- * @throws GeneralSecurityException if the context cannot be created for the
- * given key or algorithm
- */
- @Override
- protected SignatureContext newVerifyContext(final CryptoAlgorithm alg, final PublicKey key)
- throws GeneralSecurityException {
- return new Ed25519SignatureContext(alg, key);
- }
-
- /**
- * Returns the key generation specification class used by this builder.
- *
- * @return {@code Ed25519KeyGenSpec.class}
- */
- @Override
- protected Class keyGenSpecClass() {
- return Ed25519KeyGenSpec.class;
- }
-
- /**
- * Returns the public key import specification class used by this builder.
- *
- * @return {@code Ed25519PublicKeySpec.class}
- */
- @Override
- protected Class publicKeySpecClass() {
- return Ed25519PublicKeySpec.class;
- }
-
- /**
- * Returns the private key import specification class used by this builder.
- *
- * @return {@code Ed25519PrivateKeySpec.class}
- */
- @Override
- protected Class privateKeySpecClass() {
- return Ed25519PrivateKeySpec.class;
- }
-
- /**
- * Supplies a default key generation specification for Ed25519.
- *
- * @return a supplier returning {@link Ed25519KeyGenSpec#defaultSpec()}
- */
- @Override
- protected Supplier defaultKeyGenSpecSupplier() {
- return DEFAULT_GEN;
- }
-
- /**
- * Returns the currently configured key generation specification or null to
- * indicate the default should be used.
- *
- *
- * Ed25519 has no tunables in this builder, so this method returns {@code null}.
- *
- *
- * @return {@code null}
- */
- @Override
- protected Ed25519KeyGenSpec currentKeyGenSpecOrNull() {
- return null; // no tunables for Ed25519
- }
-
- /**
- * Builds a public key import specification from X.509-encoded
- * SubjectPublicKeyInfo bytes.
- *
- * @param x509 the X.509 public key bytes
- * @param ignoredProvider an optional provider hint, ignored by this
- * implementation
- * @return a new {@link Ed25519PublicKeySpec} wrapping the provided bytes
- */
- @Override
- protected Ed25519PublicKeySpec makePublicKeySpec(final byte[] x509, final String ignoredProvider) {
- return new Ed25519PublicKeySpec(x509);
- }
-
- /**
- * Builds a private key import specification from PKCS#8-encoded PrivateKeyInfo
- * bytes.
- *
- * @param pkcs8 the PKCS#8 private key bytes
- * @param ignoredProvider an optional provider hint, ignored by this
- * implementation
- * @return a new {@link Ed25519PrivateKeySpec} wrapping the provided bytes
- */
- @Override
- protected Ed25519PrivateKeySpec makePrivateKeySpec(final byte[] pkcs8, final String ignoredProvider) {
- return new Ed25519PrivateKeySpec(pkcs8);
- }
-
- /**
- * Returns the default provider hint used for key imports when none is
- * explicitly supplied.
- *
- *
- * This builder relies on the JDK default provider for Ed25519.
- *
- *
- * @return {@code null} to indicate no provider preference
- */
- @Override
- protected String defaultProviderHint() {
- return null; // use JDK default provider
- }
-}
diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/Ed448DataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/Ed448DataContentBuilder.java
deleted file mode 100644
index f8e1a5a..0000000
--- a/lib/src/main/java/zeroecho/sdk/builders/alg/Ed448DataContentBuilder.java
+++ /dev/null
@@ -1,274 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2026, Leo Galambos
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- *
- * 3. All advertising materials mentioning features or use of this software must
- * display the following acknowledgement:
- * This product includes software developed by the Egothor project.
- *
- * 4. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- ******************************************************************************/
-package zeroecho.sdk.builders.alg;
-
-import java.security.GeneralSecurityException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.util.function.Supplier;
-
-import zeroecho.core.CryptoAlgorithm;
-import zeroecho.core.alg.ed448.Ed448KeyGenSpec;
-import zeroecho.core.alg.ed448.Ed448PrivateKeySpec;
-import zeroecho.core.alg.ed448.Ed448PublicKeySpec;
-import zeroecho.core.alg.ed448.Ed448SignatureContext;
-import zeroecho.core.context.SignatureContext;
-
-/**
- * Builder for constructing streaming Ed448 signature and verification
- * {@link zeroecho.sdk.content.api.DataContent} pipelines.
- *
- *
- * {@code Ed448DataContentBuilder} is a thin adapter around
- * {@link AbstractStreamingSignatureDataBuilder} that binds the generic
- * streaming signature framework to the Ed448 algorithm. It supports both
- * signing and verification flows, with key material provided via
- * {@link Ed448KeyGenSpec}, {@link Ed448PublicKeySpec}, and
- * {@link Ed448PrivateKeySpec}.
- *
- *
- * Usage example
{@code
- * // Create a builder for signing
- * Ed448DataContentBuilder builder = Ed448DataContentBuilder.builder()
- * .sign()
- * .generateKeyPair()
- * .emitBase64Signature();
- *
- * // Build a content pipeline
- * PlainContent content = builder.build(true);
- * content.setInput(originalContent);
- *
- * try (InputStream in = content.getStream()) {
- * byte[] signature = in.readAllBytes();
- * }
- * }
- *
- * Design notes
- *
- * - Uses {@link Ed448SignatureContext} for both signing and
- * verification.
- * - Key generation is parameterized by {@link Ed448KeyGenSpec}, but Ed448
- * exposes no runtime tunables; the default is always used.
- * - Public/private key imports are supported via X.509 and PKCS#8 wrappers
- * respectively.
- * - No provider hints are necessary; the JDK default is assumed.
- *
- *
- * @see Ed448SignatureContext
- * @see Ed448KeyGenSpec
- * @see Ed448PublicKeySpec
- * @see Ed448PrivateKeySpec
- * @since 1.0
- */
-public final class Ed448DataContentBuilder
- extends AbstractStreamingSignatureDataBuilder {
-
- private static final Supplier DEFAULT_GEN = Ed448KeyGenSpec::defaultSpec;
-
- /**
- * Creates a new builder instance for constructing Ed448 streaming signature
- * pipelines.
- *
- * Example
{@code
- * Ed448DataContentBuilder b = Ed448DataContentBuilder.builder();
- * }
- *
- * @return a new {@code Ed448DataContentBuilder}
- */
- public static Ed448DataContentBuilder builder() {
- return new Ed448DataContentBuilder();
- }
-
- /**
- * Returns the canonical algorithm name used to resolve an implementation from
- * {@link zeroecho.core.CryptoAlgorithms}.
- *
- * @return the string {@code "Ed448"}
- */
- @Override
- protected String algorithmName() {
- return "Ed448";
- }
-
- /**
- * Creates a signing {@link SignatureContext} for Ed448 using the provided
- * algorithm instance and private key.
- *
- *
- * The returned context is configured to accept streaming updates and to produce
- * a detached signature tag at end-of-stream.
- *
- *
- * Example
{@code
- * SignatureContext sc = newSignContext(alg, privateKey);
- * try (InputStream in = sc.wrap(upstream)) {
- * in.transferTo(OutputStream.nullOutputStream());
- * }
- * }
- *
- * @param alg the resolved algorithm instance
- * @param key the private key used for signing
- * @return a new signature context in sign mode
- * @throws GeneralSecurityException if the context cannot be created for the
- * given key or algorithm
- */
- @Override
- protected SignatureContext newSignContext(CryptoAlgorithm alg, PrivateKey key) throws GeneralSecurityException {
- return new Ed448SignatureContext(alg, key);
- }
-
- /**
- * Creates a verification {@link SignatureContext} for Ed448 using the provided
- * algorithm instance and public key.
- *
- *
- * The returned context is configured to accept streaming updates and to
- * validate the expected detached signature tag at end-of-stream.
- *
- *
- * @param alg the resolved algorithm instance
- * @param key the public key used for verification
- * @return a new signature context in verify mode
- * @throws GeneralSecurityException if the context cannot be created for the
- * given key or algorithm
- */
- @Override
- protected SignatureContext newVerifyContext(CryptoAlgorithm alg, PublicKey key) throws GeneralSecurityException {
- return new Ed448SignatureContext(alg, key);
- }
-
- /**
- * Returns the key generation specification class used by this builder.
- *
- * @return {@code Ed448KeyGenSpec.class}
- */
- @Override
- protected Class keyGenSpecClass() {
- return Ed448KeyGenSpec.class;
- }
-
- /**
- * Returns the public key import specification class used by this builder.
- *
- * @return {@code Ed448PublicKeySpec.class}
- */
- @Override
- protected Class publicKeySpecClass() {
- return Ed448PublicKeySpec.class;
- }
-
- /**
- * Returns the private key import specification class used by this builder.
- *
- * @return {@code Ed448PrivateKeySpec.class}
- */
- @Override
- protected Class privateKeySpecClass() {
- return Ed448PrivateKeySpec.class;
- }
-
- /**
- * Supplies the default key generation specification for Ed448.
- *
- * @return a supplier returning {@link Ed448KeyGenSpec#defaultSpec()}
- */
- @Override
- protected Supplier defaultKeyGenSpecSupplier() {
- return DEFAULT_GEN;
- }
-
- /**
- * Returns the currently configured key generation specification or null to
- * indicate that the default should be used.
- *
- *
- * Ed448 has no tunables in this builder, so this method returns {@code null}.
- *
- *
- * @return {@code null}
- */
- @Override
- protected Ed448KeyGenSpec currentKeyGenSpecOrNull() {
- return null; // no options
- }
-
- /**
- * Builds a public key import specification from X.509-encoded
- * SubjectPublicKeyInfo bytes.
- *
- * Example
{@code
- * Ed448PublicKeySpec spec = makePublicKeySpec(spkiBytes, null);
- * }
- *
- * @param x509 X.509 public key bytes (SubjectPublicKeyInfo)
- * @param ignoredProvider optional provider hint, ignored by this implementation
- * @return a new {@link Ed448PublicKeySpec} wrapping the provided bytes
- */
- @Override
- protected Ed448PublicKeySpec makePublicKeySpec(byte[] x509, String ignoredProvider) {
- return new Ed448PublicKeySpec(x509);
- }
-
- /**
- * Builds a private key import specification from PKCS#8-encoded PrivateKeyInfo
- * bytes.
- *
- * Example
{@code
- * Ed448PrivateKeySpec spec = makePrivateKeySpec(pkcs8Bytes, null);
- * }
- *
- * @param pkcs8 PKCS#8 private key bytes (PrivateKeyInfo)
- * @param ignoredProvider optional provider hint, ignored by this implementation
- * @return a new {@link Ed448PrivateKeySpec} wrapping the provided bytes
- */
- @Override
- protected Ed448PrivateKeySpec makePrivateKeySpec(byte[] pkcs8, String ignoredProvider) {
- return new Ed448PrivateKeySpec(pkcs8);
- }
-
- /**
- * Returns the default provider hint used for key imports when none is
- * explicitly supplied.
- *
- *
- * This builder relies on the JDK default provider for Ed448.
- *
- *
- * @return {@code null} to indicate no provider preference
- */
- @Override
- protected String defaultProviderHint() {
- return null; // JDK default
- }
-}
diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/ElgamalEncDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/ElgamalEncDataContentBuilder.java
index 32b097e..d7571dc 100644
--- a/lib/src/main/java/zeroecho/sdk/builders/alg/ElgamalEncDataContentBuilder.java
+++ b/lib/src/main/java/zeroecho/sdk/builders/alg/ElgamalEncDataContentBuilder.java
@@ -33,6 +33,8 @@
******************************************************************************/
package zeroecho.sdk.builders.alg;
+import zeroecho.sdk.ZeroEchoSession;
+
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
@@ -41,8 +43,6 @@ import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Objects;
-import zeroecho.core.CryptoAlgorithm;
-import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage;
import zeroecho.core.alg.elgamal.ElgamalEncSpec;
import zeroecho.core.alg.elgamal.ElgamalKeyGenSpec;
@@ -50,7 +50,6 @@ import zeroecho.core.alg.elgamal.ElgamalParamSpec;
import zeroecho.core.alg.elgamal.ElgamalPrivateKeySpec;
import zeroecho.core.alg.elgamal.ElgamalPublicKeySpec;
import zeroecho.core.context.EncryptionContext;
-import zeroecho.core.spi.AsymmetricKeyBuilder;
import zeroecho.sdk.builders.core.DataContentBuilder;
import zeroecho.sdk.content.api.DataContent;
import zeroecho.sdk.content.api.EncryptedContent;
@@ -109,6 +108,8 @@ import zeroecho.sdk.content.api.PlainContent;
* @see ElgamalParamSpec
*/
public final class ElgamalEncDataContentBuilder implements DataContentBuilder {
+ private static final String ALGORITHM_ID = "ElGamal";
+ private final ZeroEchoSession session;
private PublicKey publicKey;
private PrivateKey privateKey;
@@ -123,7 +124,8 @@ public final class ElgamalEncDataContentBuilder implements DataContentBuilder b = alg.asymmetricKeyBuilder(ElgamalKeyGenSpec.class);
- KeyPair kp = b.generateKeyPair(keyGen);
+ KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair(ALGORITHM_ID, keyGen);
this.publicKey = kp.getPublic();
this.privateKey = kp.getPrivate();
} else if (genKeyPairPredef) {
- AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(ElgamalParamSpec.class);
- KeyPair kp = b.generateKeyPair(paramSpec);
+ KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair(ALGORITHM_ID, paramSpec);
this.publicKey = kp.getPublic();
this.privateKey = kp.getPrivate();
}
if (importPrivatePkcs8 != null) {
- AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(ElgamalPrivateKeySpec.class);
- this.privateKey = b.importPrivate(new ElgamalPrivateKeySpec(importPrivatePkcs8));
+ this.privateKey = session.keyBuilders().asymmetric().importPrivate(ALGORITHM_ID,
+ new ElgamalPrivateKeySpec(importPrivatePkcs8));
}
if (importPublicX509 != null) {
- AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(ElgamalPublicKeySpec.class);
- this.publicKey = b.importPublic(new ElgamalPublicKeySpec(importPublicX509));
+ this.publicKey = session.keyBuilders().asymmetric().importPublic(ALGORITHM_ID,
+ new ElgamalPublicKeySpec(importPublicX509));
}
}
@@ -363,7 +362,8 @@ public final class ElgamalEncDataContentBuilder implements DataContentBuilder
@@ -371,7 +371,7 @@ public final class ElgamalEncDataContentBuilder implements DataContentBuilder