refactor!: consolidate crypto architecture and security model

* make ZeroEchoSession the sole policy, audit, and runtime boundary
* replace combined key builders with operation-specific SPI and typed metadata
* remove obsolete pre-release compatibility APIs and global crypto operations
* finalize JCA agreement contexts and replace inheritance with composition
* harden secret lifecycle, key destruction, hybrid KEX, PBKDF2, and audit handling
* standardize PairSeq I/O and introduce immutable validated value types
* migrate app, ext, samples, and required pki integration points
* expand correctness, security, concurrency, and malformed-input coverage

BREAKING CHANGE: removes deprecated pre-release global configuration, legacy
context factories, combined key-builder contracts, String-based password APIs,
unchecked PairSeq writing, BlockGeometry public fields, and other compatibility
facades.
This commit is contained in:
2026-07-28 19:20:30 +02:00
parent 7319aca0db
commit 49dc080c65
298 changed files with 12802 additions and 8763 deletions

View File

@@ -40,7 +40,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.SecureRandom; import java.util.Arrays;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.Locale; import java.util.Locale;
@@ -52,17 +52,21 @@ import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options; import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException; import org.apache.commons.cli.ParseException;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyUsage; import zeroecho.core.KeyUsage;
import zeroecho.core.context.EncryptionContext; import zeroecho.core.context.EncryptionContext;
import zeroecho.core.context.KemContext; import zeroecho.core.context.KemContext;
import zeroecho.core.err.UnsupportedRoleException;
import zeroecho.core.storage.KeyringStore; import zeroecho.core.storage.KeyringStore;
import zeroecho.sdk.Pbkdf2Limits;
import zeroecho.sdk.ZeroEchoSession;
import zeroecho.sdk.builders.alg.AesDataContentBuilder; import zeroecho.sdk.builders.alg.AesDataContentBuilder;
import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder; import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder;
import zeroecho.sdk.builders.core.PlainFileBuilder; import zeroecho.sdk.builders.core.PlainFileBuilder;
import zeroecho.sdk.content.api.DataContent; import zeroecho.sdk.guard.MultiRecipientContent;
import zeroecho.sdk.guard.MultiRecipientDataSourceBuilder; import zeroecho.sdk.guard.MultiRecipientDataSourceBuilder;
import zeroecho.sdk.guard.RecipientKekSizes;
import zeroecho.sdk.guard.UnlockMaterial; import zeroecho.sdk.guard.UnlockMaterial;
import zeroecho.sdk.util.RandomSupport;
/** /**
* Guard is a unified subcommand that encrypts and decrypts using a * Guard is a unified subcommand that encrypts and decrypts using a
@@ -97,6 +101,8 @@ import zeroecho.sdk.guard.UnlockMaterial;
* ZeroEcho -G --encrypt in.bin --ks keys.txt \ * ZeroEcho -G --encrypt in.bin --ks keys.txt \
* --to-alias alice --to-alias bob \ * --to-alias alice --to-alias bob \
* --decoy-psw-rand 2 \ * --decoy-psw-rand 2 \
* --pbkdf2-max "$PBKDF2_POLICY_MAX" \
* --pbkdf2-hard-max "$PBKDF2_HARD_MAX" \
* --alg aes-gcm --tag-bits 128 --aad-hex 01ff * --alg aes-gcm --tag-bits 128 --aad-hex 01ff
* *
* # Decrypt with private key (payload is ChaCha20-Poly1305) * # Decrypt with private key (payload is ChaCha20-Poly1305)
@@ -124,7 +130,6 @@ public final class Guard {
*/ */
public static int main(final String[] args, final Options options) // NOPMD public static int main(final String[] args, final Options options) // NOPMD
throws ParseException, IOException, GeneralSecurityException { throws ParseException, IOException, GeneralSecurityException {
// ---- operation selection // ---- operation selection
final Option OPT_ENCRYPT = Option.builder("e").longOpt("encrypt").hasArg().argName("in-file") final Option OPT_ENCRYPT = Option.builder("e").longOpt("encrypt").hasArg().argName("in-file")
.desc("Encrypt the given file").get(); .desc("Encrypt the given file").get();
@@ -177,7 +182,12 @@ public final class Guard {
final Option OPT_PSW_SALT = Option.builder().longOpt("to-salt-len").hasArg().argName("bytes") final Option OPT_PSW_SALT = Option.builder().longOpt("to-salt-len").hasArg().argName("bytes")
.desc("PBKDF2 salt length for password recipients (default 16)").get(); .desc("PBKDF2 salt length for password recipients (default 16)").get();
final Option OPT_PSW_KEK = Option.builder().longOpt("to-kek-bytes").hasArg().argName("bytes") final Option OPT_PSW_KEK = Option.builder().longOpt("to-kek-bytes").hasArg().argName("bytes")
.desc("Derived KEK length for password recipients (default 32)").get(); .desc("Recipient KEK length: exactly 16 or 32 bytes (default 32)").get();
final Option OPT_PBKDF2_MAX = Option.builder().longOpt("pbkdf2-max").hasArg().argName("iterations")
.desc("Operational PBKDF2 ceiling; required for password operations").get();
final Option OPT_PBKDF2_HARD_MAX = Option.builder().longOpt("pbkdf2-hard-max").hasArg()
.argName("iterations")
.desc("Absolute decoded PBKDF2 safety ceiling; required for password operations").get();
// ---- decoys (all types) // ---- decoys (all types)
final Option OPT_DECOY_ALIAS = Option.builder().longOpt("decoy-alias").hasArg().argName("alias") final Option OPT_DECOY_ALIAS = Option.builder().longOpt("decoy-alias").hasArg().argName("alias")
@@ -215,6 +225,8 @@ public final class Guard {
options.addOption(OPT_PSW_ITER); options.addOption(OPT_PSW_ITER);
options.addOption(OPT_PSW_SALT); options.addOption(OPT_PSW_SALT);
options.addOption(OPT_PSW_KEK); options.addOption(OPT_PSW_KEK);
options.addOption(OPT_PBKDF2_MAX);
options.addOption(OPT_PBKDF2_HARD_MAX);
options.addOption(OPT_DECOY_ALIAS); options.addOption(OPT_DECOY_ALIAS);
options.addOption(OPT_DECOY_PSW); options.addOption(OPT_DECOY_PSW);
@@ -225,6 +237,10 @@ public final class Guard {
final CommandLineParser parser = new DefaultParser(); final CommandLineParser parser = new DefaultParser();
final CommandLine cmd = parser.parse(options, args); final CommandLine cmd = parser.parse(options, args);
final boolean passwordOperation = cmd.hasOption(OPT_TO_PSW) || cmd.hasOption(OPT_DECOY_PSW)
|| cmd.hasOption(OPT_DECOY_PSW_RAND) || cmd.hasOption(OPT_PASSWORD);
final ZeroEchoSession session = createSession(cmd, OPT_PBKDF2_MAX, OPT_PBKDF2_HARD_MAX,
passwordOperation);
final boolean encrypt = cmd.hasOption(OPT_ENCRYPT); final boolean encrypt = cmd.hasOption(OPT_ENCRYPT);
final Path inPath = Paths.get(cmd.getOptionValue(encrypt ? OPT_ENCRYPT : OPT_DECRYPT)); final Path inPath = Paths.get(cmd.getOptionValue(encrypt ? OPT_ENCRYPT : OPT_DECRYPT));
@@ -249,7 +265,7 @@ public final class Guard {
final ChaChaDataContentBuilder chacha; final ChaChaDataContentBuilder chacha;
switch (alg) { switch (alg) {
case "aes-gcm" -> { case "aes-gcm" -> {
aes = AesDataContentBuilder.builder().modeGcm(tagBits); aes = AesDataContentBuilder.builder(session).modeGcm(tagBits);
if (header) { if (header) {
aes.withHeader(); aes.withHeader();
} }
@@ -259,28 +275,28 @@ public final class Guard {
chacha = null; chacha = null;
} }
case "aes-ctr" -> { case "aes-ctr" -> {
aes = AesDataContentBuilder.builder().modeCtr(); aes = AesDataContentBuilder.builder(session).modeCtr();
if (header) { if (header) {
aes.withHeader(); aes.withHeader();
} }
chacha = null; chacha = null;
} }
case "aes-cbc-pkcs7" -> { case "aes-cbc-pkcs7" -> {
aes = AesDataContentBuilder.builder().modeCbcPkcs5(); aes = AesDataContentBuilder.builder(session).modeCbcPkcs5();
if (header) { if (header) {
aes.withHeader(); aes.withHeader();
} }
chacha = null; chacha = null;
} }
case "aes-cbc-nopad" -> { case "aes-cbc-nopad" -> {
aes = AesDataContentBuilder.builder().modeCbcNoPadding(); aes = AesDataContentBuilder.builder(session).modeCbcNoPadding();
if (header) { if (header) {
aes.withHeader(); aes.withHeader();
} }
chacha = null; chacha = null;
} }
case "chacha-aead" -> { case "chacha-aead" -> {
chacha = ChaChaDataContentBuilder.builder(); chacha = ChaChaDataContentBuilder.builder(session);
// selecting AEAD: if the user did not supply AAD, pass empty to pick AEAD // selecting AEAD: if the user did not supply AAD, pass empty to pick AEAD
chacha.withAad(aad != null ? aad : new byte[0]); chacha.withAad(aad != null ? aad : new byte[0]);
if (header) { if (header) {
@@ -302,7 +318,7 @@ public final class Guard {
aes = null; aes = null;
} }
case "chacha-stream" -> { case "chacha-stream" -> {
chacha = ChaChaDataContentBuilder.builder(); chacha = ChaChaDataContentBuilder.builder(session);
if (header) { if (header) {
chacha.withHeader(); chacha.withHeader();
} }
@@ -321,71 +337,93 @@ public final class Guard {
} }
// envelope builder (new API) // envelope builder (new API)
final MultiRecipientDataSourceBuilder env = new MultiRecipientDataSourceBuilder().payloadKeyBytes(cekBytes) final MultiRecipientDataSourceBuilder env = MultiRecipientDataSourceBuilder.builder(session)
.payloadKeyBytes(cekBytes)
.headerLimits(maxRecipients, maxEntryLen); .headerLimits(maxRecipients, maxEntryLen);
if (aes != null) { UnlockMaterial borrowedUnlockMaterial = null;
env.withAes(aes); try (env) {
} else { if (aes != null) {
env.withChaCha(chacha); env.withAes(aes);
}
// shuffle on by default
if (shuffle) {
env.shuffle();
}
// recipients and decoys only apply on encrypt
if (encrypt) {
final int iter = Integer.parseInt(cmd.getOptionValue(OPT_PSW_ITER, "200000"));
final int saltLen = Integer.parseInt(cmd.getOptionValue(OPT_PSW_SALT, "16"));
final int kekLen = Integer.parseInt(cmd.getOptionValue(OPT_PSW_KEK, "32"));
final KeyringStore ks = loadKeyringIfPresent(cmd, OPT_KEYRING);
// real recipients by alias
for (String alias : cmd.getOptionValues(OPT_TO_ALIAS) == null ? new String[0]
: cmd.getOptionValues(OPT_TO_ALIAS)) {
addRecipientFromAlias(env, ks, alias, kekLen, saltLen, false);
}
// real password recipients
for (String psw : cmd.getOptionValues(OPT_TO_PSW) == null ? new String[0]
: cmd.getOptionValues(OPT_TO_PSW)) {
env.addPasswordRecipient(psw.toCharArray(), iter, saltLen, kekLen);
}
// decoys by alias (key types)
for (String alias : cmd.getOptionValues(OPT_DECOY_ALIAS) == null ? new String[0]
: cmd.getOptionValues(OPT_DECOY_ALIAS)) {
addRecipientFromAlias(env, ks, alias, kekLen, saltLen, true);
}
// decoy passwords (explicit)
for (String psw : cmd.getOptionValues(OPT_DECOY_PSW) == null ? new String[0]
: cmd.getOptionValues(OPT_DECOY_PSW)) {
env.addPasswordRecipientDecoy(psw.toCharArray(), iter, saltLen, kekLen);
}
// decoy passwords (random)
final int rndCount = Integer.parseInt(cmd.getOptionValue(OPT_DECOY_PSW_RAND, "0"));
for (int i = 0; i < rndCount; i++) {
env.addPasswordRecipientDecoy(randomPassword(), iter, saltLen, kekLen);
}
} else {
// unlock material for decrypt
final String privAlias = cmd.getOptionValue(OPT_PRIV_ALIAS);
final String password = cmd.getOptionValue(OPT_PASSWORD);
if ((privAlias == null && password == null) || (privAlias != null && password != null)) {
throw new ParseException("Specify exactly one of --priv-alias or --password for decryption");
}
if (privAlias != null) {
final KeyringStore ks = requireKeyring(cmd, OPT_KEYRING);
final KeyringStore.PrivateWithId pr = ks.getPrivateWithId(privAlias);
env.unlockWith(new UnlockMaterial.Private(pr.key()));
} else { } else {
env.unlockWith(new UnlockMaterial.Password(password.toCharArray())); env.withChaCha(chacha);
} }
} if (shuffle) {
env.shuffle();
}
if (encrypt) {
final int iter = Integer.parseInt(cmd.getOptionValue(OPT_PSW_ITER, "200000"));
final int saltLen = Integer.parseInt(cmd.getOptionValue(OPT_PSW_SALT, "16"));
final int kekLen = RecipientKekSizes.requireSupported(
Integer.parseInt(cmd.getOptionValue(OPT_PSW_KEK, "32")));
// connect upstream and run final KeyringStore ks = loadKeyringIfPresent(session, cmd, OPT_KEYRING);
final DataContent content = env.build(encrypt); // env installs default openers on decrypt if none were added for (String alias : cmd.getOptionValues(OPT_TO_ALIAS) == null ? new String[0]
content.setInput(PlainFileBuilder.builder().url(inPath.toUri().toURL()).build(encrypt)); : cmd.getOptionValues(OPT_TO_ALIAS)) {
try (InputStream in = content.getStream(); OutputStream out = Files.newOutputStream(outPath)) { addRecipientFromAlias(session, env, ks, alias, kekLen, saltLen, false);
in.transferTo(out); }
for (String psw : cmd.getOptionValues(OPT_TO_PSW) == null ? new String[0]
: cmd.getOptionValues(OPT_TO_PSW)) {
char[] passwordChars = psw.toCharArray();
try {
env.addPasswordRecipient(passwordChars, iter, saltLen, kekLen);
} finally {
Arrays.fill(passwordChars, '\0');
}
}
for (String alias : cmd.getOptionValues(OPT_DECOY_ALIAS) == null ? new String[0]
: cmd.getOptionValues(OPT_DECOY_ALIAS)) {
addRecipientFromAlias(session, env, ks, alias, kekLen, saltLen, true);
}
for (String psw : cmd.getOptionValues(OPT_DECOY_PSW) == null ? new String[0]
: cmd.getOptionValues(OPT_DECOY_PSW)) {
char[] passwordChars = psw.toCharArray();
try {
env.addPasswordRecipientDecoy(passwordChars, iter, saltLen, kekLen);
} finally {
Arrays.fill(passwordChars, '\0');
}
}
final int rndCount = Integer.parseInt(cmd.getOptionValue(OPT_DECOY_PSW_RAND, "0"));
for (int i = 0; i < rndCount; i++) {
char[] passwordChars = randomPassword();
try {
env.addPasswordRecipientDecoy(passwordChars, iter, saltLen, kekLen);
} finally {
Arrays.fill(passwordChars, '\0');
}
}
} else {
final String privAlias = cmd.getOptionValue(OPT_PRIV_ALIAS);
final String password = cmd.getOptionValue(OPT_PASSWORD);
if ((privAlias == null && password == null) || (privAlias != null && password != null)) {
throw new ParseException("Specify exactly one of --priv-alias or --password for decryption");
}
if (privAlias != null) {
final KeyringStore ks = requireKeyring(session, cmd, OPT_KEYRING);
final KeyringStore.PrivateWithId pr = ks.getPrivateWithId(privAlias);
borrowedUnlockMaterial = new UnlockMaterial.Private(pr.key());
} else {
char[] passwordChars = password.toCharArray();
try {
borrowedUnlockMaterial = new UnlockMaterial.Password(passwordChars);
} finally {
Arrays.fill(passwordChars, '\0');
}
}
env.unlockWith(borrowedUnlockMaterial);
}
try (MultiRecipientContent content = env.build(encrypt)) {
content.setInput(PlainFileBuilder.builder().url(inPath.toUri().toURL()).build(encrypt));
try (InputStream in = content.getStream(); OutputStream out = Files.newOutputStream(outPath)) {
in.transferTo(out);
}
}
} finally {
if (borrowedUnlockMaterial instanceof UnlockMaterial.Password passwordMaterial) {
passwordMaterial.destroy();
}
} }
return 0; return 0;
@@ -404,6 +442,33 @@ public final class Guard {
return Paths.get(s + suffix); return Paths.get(s + suffix);
} }
private static ZeroEchoSession createSession(CommandLine cmd, Option operationalMaximumOption,
Option absoluteMaximumOption, boolean passwordOperation) throws ParseException {
boolean hasOperationalMaximum = cmd.hasOption(operationalMaximumOption);
boolean hasAbsoluteMaximum = cmd.hasOption(absoluteMaximumOption);
if (!hasOperationalMaximum && !hasAbsoluteMaximum) {
if (passwordOperation) {
throw new ParseException(
"Password operations require --pbkdf2-max and --pbkdf2-hard-max");
}
return new ZeroEchoSession();
}
if (!hasOperationalMaximum || !hasAbsoluteMaximum) {
throw new ParseException("--pbkdf2-max and --pbkdf2-hard-max must be specified together");
}
try {
int operationalMaximum = Integer.parseInt(cmd.getOptionValue(operationalMaximumOption));
int absoluteMaximum = Integer.parseInt(cmd.getOptionValue(absoluteMaximumOption));
return new ZeroEchoSession().withPbkdf2Limits(
new Pbkdf2Limits(operationalMaximum, absoluteMaximum));
} catch (IllegalArgumentException exception) {
ParseException parseException =
new ParseException("Invalid PBKDF2 limits: " + exception.getMessage());
parseException.initCause(exception);
throw parseException;
}
}
private static byte[] parseHex(String s) throws ParseException { private static byte[] parseHex(String s) throws ParseException {
try { try {
return HexFormat.of().parseHex(s); return HexFormat.of().parseHex(s);
@@ -434,15 +499,14 @@ public final class Guard {
* *
* <p> * <p>
* In both cases, the created context is consumed by * In both cases, the created context is consumed by
* {@link MultiRecipientDataSourceBuilder#addRecipient(Object)} and is closed * the matching {@link MultiRecipientDataSourceBuilder} recipient method and is
* internally by the builder. * closed internally by the resulting content.
* </p> * </p>
* *
* @param env target builder to which the recipient is added * @param env target builder to which the recipient is added
* @param ks keyring store that provides public keys by alias * @param ks keyring store that provides public keys by alias
* @param alias alias name of the recipient's public key in the keyring * @param alias alias name of the recipient's public key in the keyring
* @param kekBytes desired length in bytes of the key-encryption key when using * @param kekBytes key-encryption key length; exactly 16 or 32 bytes
* KEM
* @param saltLen salt length in bytes when using KEM * @param saltLen salt length in bytes when using KEM
* @param decoy whether the recipient is a decoy * @param decoy whether the recipient is a decoy
* @throws GeneralSecurityException if the algorithm does not support the * @throws GeneralSecurityException if the algorithm does not support the
@@ -450,29 +514,52 @@ public final class Guard {
* @throws IOException if context creation or builder operations * @throws IOException if context creation or builder operations
* require I/O and fail * require I/O and fail
*/ */
private static void addRecipientFromAlias(MultiRecipientDataSourceBuilder env, KeyringStore ks, String alias, @SuppressWarnings({ "PMD.CloseResource", "PMD.UseTryWithResources" })
private static void addRecipientFromAlias(ZeroEchoSession session, MultiRecipientDataSourceBuilder env,
KeyringStore ks, String alias,
int kekBytes, int saltLen, boolean decoy) throws GeneralSecurityException, IOException { int kekBytes, int saltLen, boolean decoy) throws GeneralSecurityException, IOException {
KeyringStore.PublicWithId r = ks.getPublicWithId(alias); KeyringStore.PublicWithId r = ks.getPublicWithId(alias);
final String algId = r.algorithm(); final String algId = r.algorithm();
final java.security.PublicKey pub = r.key(); final java.security.PublicKey pub = r.key();
// Try KEM first KemContext kem;
try (KemContext kem = CryptoAlgorithms.create(algId, KeyUsage.ENCAPSULATE, pub)) { try {
if (decoy) { kem = session.createContext(algId, KeyUsage.ENCAPSULATE, pub);
env.addRecipientDecoy(kem, kekBytes, saltLen); // builder closes context } catch (UnsupportedRoleException notKem) {
} else { addEncryptionRecipient(session, env, algId, pub, decoy);
env.addRecipient(kem, kekBytes, saltLen); // builder closes context
}
return; return;
} catch (Exception notKem) { // NOPMD
// fall back to public-key encryption
} }
try (EncryptionContext enc = CryptoAlgorithms.create(algId, KeyUsage.ENCRYPT, pub)) { boolean transferred = false;
try {
if (decoy) { if (decoy) {
env.addRecipientDecoy(enc); // builder closes context env.addRecipientDecoy(kem, kekBytes, saltLen);
} else { } else {
env.addRecipient(enc); // builder closes context env.addRecipient(kem, kekBytes, saltLen);
}
transferred = true;
} finally {
if (!transferred) {
kem.close();
}
}
}
@SuppressWarnings({ "PMD.CloseResource", "PMD.UseTryWithResources" })
private static void addEncryptionRecipient(ZeroEchoSession session, MultiRecipientDataSourceBuilder env,
String algorithmId, java.security.PublicKey publicKey, boolean decoy) throws IOException {
EncryptionContext encryption = session.createContext(algorithmId, KeyUsage.ENCRYPT, publicKey);
boolean transferred = false;
try {
if (decoy) {
env.addRecipientDecoy(encryption);
} else {
env.addRecipient(encryption);
}
transferred = true;
} finally {
if (!transferred) {
encryption.close();
} }
} }
} }
@@ -480,26 +567,27 @@ public final class Guard {
private static char[] randomPassword() { private static char[] randomPassword() {
// simple random alnum for decoy purposes only // simple random alnum for decoy purposes only
final String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; final String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
final SecureRandom rnd = new SecureRandom(); final int len = 16 + RandomSupport.nextInt(17); // 16..32
final int len = 16 + rnd.nextInt(17); // 16..32
final char[] out = new char[len]; final char[] out = new char[len];
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
out[i] = alphabet.charAt(rnd.nextInt(alphabet.length())); out[i] = alphabet.charAt(RandomSupport.nextInt(alphabet.length()));
} }
return out; return out;
} }
private static KeyringStore loadKeyringIfPresent(CommandLine cmd, Option optKs) throws IOException { private static KeyringStore loadKeyringIfPresent(ZeroEchoSession session, CommandLine cmd, Option optKs)
throws IOException {
if (!cmd.hasOption(optKs)) { if (!cmd.hasOption(optKs)) {
return new KeyringStore(); return new KeyringStore(session);
} }
return KeyringStore.load(Paths.get(cmd.getOptionValue(optKs))); return KeyringStore.load(session, Paths.get(cmd.getOptionValue(optKs)));
} }
private static KeyringStore requireKeyring(CommandLine cmd, Option optKs) throws IOException, ParseException { private static KeyringStore requireKeyring(ZeroEchoSession session, CommandLine cmd, Option optKs)
throws IOException, ParseException {
if (!cmd.hasOption(optKs)) { if (!cmd.hasOption(optKs)) {
throw new ParseException("--keyring <file> is required when aliases are used"); throw new ParseException("--keyring <file> is required when aliases are used");
} }
return KeyringStore.load(Paths.get(cmd.getOptionValue(optKs))); return KeyringStore.load(session, Paths.get(cmd.getOptionValue(optKs)));
} }
} }

View File

@@ -63,6 +63,7 @@ import zeroecho.core.storage.KeyringStore;
import zeroecho.sdk.builders.alg.AesDataContentBuilder; import zeroecho.sdk.builders.alg.AesDataContentBuilder;
import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder; import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder;
import zeroecho.sdk.builders.alg.KemDataContentBuilder; import zeroecho.sdk.builders.alg.KemDataContentBuilder;
import zeroecho.sdk.ZeroEchoSession;
import zeroecho.sdk.builders.core.DataContentChainBuilder; import zeroecho.sdk.builders.core.DataContentChainBuilder;
import zeroecho.sdk.builders.core.PlainFileBuilder; import zeroecho.sdk.builders.core.PlainFileBuilder;
import zeroecho.sdk.content.api.DataContent; import zeroecho.sdk.content.api.DataContent;
@@ -203,6 +204,7 @@ public final class Kem { // NOPMD
} }
public static int main(String[] args, Options opts) throws ParseException, IOException, GeneralSecurityException { // NOPMD public static int main(String[] args, Options opts) throws ParseException, IOException, GeneralSecurityException { // NOPMD
ZeroEchoSession session = new ZeroEchoSession();
defineOptions(opts); defineOptions(opts);
CommandLineParser parser = new DefaultParser(); CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(opts, args); CommandLine cmd = parser.parse(opts, args);
@@ -240,10 +242,10 @@ public final class Kem { // NOPMD
final String kemId = cmd.getOptionValue(OPT_KEM.getLongOpt()); final String kemId = cmd.getOptionValue(OPT_KEM.getLongOpt());
final Path keyringPath = Path.of(cmd.getOptionValue(OPT_KEYRING.getLongOpt())); final Path keyringPath = Path.of(cmd.getOptionValue(OPT_KEYRING.getLongOpt()));
final KeyringStore keyring = KeyringStore.load(keyringPath); final KeyringStore keyring = KeyringStore.load(session, keyringPath);
// Configure KEM envelope // Configure KEM envelope
KemDataContentBuilder kem = KemDataContentBuilder.builder().kem(kemId); KemDataContentBuilder kem = KemDataContentBuilder.builder(session).kem(kemId);
if (cmd.hasOption(OPT_DIRECT.getLongOpt())) { if (cmd.hasOption(OPT_DIRECT.getLongOpt())) {
kem = kem.directSecret(); kem = kem.directSecret();
} else { } else {
@@ -267,7 +269,7 @@ public final class Kem { // NOPMD
// AES payload // AES payload
if (wantAes) { if (wantAes) {
String mode = cmd.getOptionValue(OPT_AES_CIPHER.getLongOpt(), "gcm").toLowerCase(Locale.ROOT); String mode = cmd.getOptionValue(OPT_AES_CIPHER.getLongOpt(), "gcm").toLowerCase(Locale.ROOT);
AesDataContentBuilder aes = AesDataContentBuilder.builder(); AesDataContentBuilder aes = AesDataContentBuilder.builder(session);
switch (mode) { switch (mode) {
case "gcm" -> { case "gcm" -> {
Integer tagBitsOpt = parsedIntOpt(cmd, OPT_AES_TAG_BITS); Integer tagBitsOpt = parsedIntOpt(cmd, OPT_AES_TAG_BITS);
@@ -294,7 +296,7 @@ public final class Kem { // NOPMD
// ChaCha payload // ChaCha payload
if (wantChaCha) { if (wantChaCha) {
ChaChaDataContentBuilder cc = ChaChaDataContentBuilder.builder(); ChaChaDataContentBuilder cc = ChaChaDataContentBuilder.builder(session);
byte[] nonce = parseHexOpt(cmd, OPT_CHACHA_NONCE); byte[] nonce = parseHexOpt(cmd, OPT_CHACHA_NONCE);
if (nonce != null) { if (nonce != null) {
cc = cc.withNonce(nonce); cc = cc.withNonce(nonce);

View File

@@ -42,6 +42,7 @@ import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyPair; import java.security.KeyPair;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
@@ -61,10 +62,11 @@ import org.apache.commons.cli.ParseException;
import zeroecho.core.CryptoAlgorithm; import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.CryptoAlgorithms; import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyOperation;
import zeroecho.core.KeyOperationInfo;
import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.AlgorithmKeySpec;
import zeroecho.core.spi.AsymmetricKeyBuilder;
import zeroecho.core.spi.SymmetricKeyBuilder;
import zeroecho.core.storage.KeyringStore; import zeroecho.core.storage.KeyringStore;
import zeroecho.sdk.ZeroEchoSession;
/** /**
* Command-line utility for managing key material in a text-based keyring store. * Command-line utility for managing key material in a text-based keyring store.
@@ -231,6 +233,7 @@ public final class KeyStoreManagement { // NOPMD
* @throws IOException if I/O fails * @throws IOException if I/O fails
*/ */
public static int main(final String[] args, final Options dispatcherOptions) throws ParseException, IOException { public static int main(final String[] args, final Options dispatcherOptions) throws ParseException, IOException {
ZeroEchoSession session = new ZeroEchoSession();
defineOptions(dispatcherOptions); defineOptions(dispatcherOptions);
CommandLineParser parser = new DefaultParser(); CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(dispatcherOptions, args); CommandLine cmd = parser.parse(dispatcherOptions, args);
@@ -245,14 +248,15 @@ public final class KeyStoreManagement { // NOPMD
} }
Path keyringPath = Path.of(cmd.getOptionValue(KEYSTORE_OPTION.getLongOpt())); Path keyringPath = Path.of(cmd.getOptionValue(KEYSTORE_OPTION.getLongOpt()));
KeyringStore store = Files.exists(keyringPath) ? KeyringStore.load(keyringPath) : new KeyringStore(); KeyringStore store = Files.exists(keyringPath) ? KeyringStore.load(session, keyringPath)
: new KeyringStore(session);
if (cmd.hasOption(LIST_ALIASES_OPTION.getLongOpt())) { if (cmd.hasOption(LIST_ALIASES_OPTION.getLongOpt())) {
listAliases(store); listAliases(store);
return 0; return 0;
} }
if (cmd.hasOption(GENERATE_OPTION.getLongOpt())) { if (cmd.hasOption(GENERATE_OPTION.getLongOpt())) {
doGenerate(store, cmd); doGenerate(session, store, cmd);
store.save(keyringPath); store.save(keyringPath);
return 0; return 0;
} }
@@ -311,8 +315,12 @@ public final class KeyStoreManagement { // NOPMD
Set<String> ids = CryptoAlgorithms.available(); Set<String> ids = CryptoAlgorithms.available();
for (String id : ids) { for (String id : ids) {
CryptoAlgorithm a = CryptoAlgorithms.require(id); CryptoAlgorithm a = CryptoAlgorithms.require(id);
boolean hasAsym = !a.asymmetricBuildersInfo().isEmpty(); boolean hasAsym = a.keyOperations().stream()
boolean hasSym = !a.symmetricBuildersInfo().isEmpty(); .anyMatch(info -> info.operation() != KeyOperation.SYMMETRIC_GENERATE
&& info.operation() != KeyOperation.SYMMETRIC_IMPORT);
boolean hasSym = a.keyOperations().stream()
.anyMatch(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE
|| info.operation() == KeyOperation.SYMMETRIC_IMPORT);
out.printf(Locale.ROOT, "%-12s asym:%s sym:%s%n", id, hasAsym, hasSym); out.printf(Locale.ROOT, "%-12s asym:%s sym:%s%n", id, hasAsym, hasSym);
} }
} }
@@ -346,7 +354,8 @@ public final class KeyStoreManagement { // NOPMD
* @param store keyring store to mutate * @param store keyring store to mutate
* @param cmd parsed command line * @param cmd parsed command line
*/ */
public static void doGenerate(final KeyringStore store, final CommandLine cmd) { // NOPMD public static void doGenerate(final ZeroEchoSession session, final KeyringStore store,
final CommandLine cmd) {
String algId = required(cmd, ALG_OPTION, "--alg is required for --generate"); String algId = required(cmd, ALG_OPTION, "--alg is required for --generate");
String aliasBase = required(cmd, ALIAS_OPTION, "--alias is required for --generate"); String aliasBase = required(cmd, ALIAS_OPTION, "--alias is required for --generate");
String kind = cmd.getOptionValue(KIND_OPTION.getLongOpt()); String kind = cmd.getOptionValue(KIND_OPTION.getLongOpt());
@@ -355,119 +364,155 @@ public final class KeyStoreManagement { // NOPMD
boolean overwrite = cmd.hasOption(OVERWRITE_OPTION.getLongOpt()); boolean overwrite = cmd.hasOption(OVERWRITE_OPTION.getLongOpt());
CryptoAlgorithm alg = CryptoAlgorithms.require(algId); CryptoAlgorithm alg = CryptoAlgorithms.require(algId);
boolean canAsym = !alg.asymmetricBuildersInfo().isEmpty(); boolean canAsym = alg.keyOperations().stream()
boolean canSym = !alg.symmetricBuildersInfo().isEmpty(); .anyMatch(info -> info.operation() == KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE);
boolean canSym = alg.keyOperations().stream()
.anyMatch(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE);
boolean doAsym = "asym".equalsIgnoreCase(kind) || (kind == null && canAsym && !canSym); GenerationKind generationKind = selectGenerationKind(kind, canAsym, canSym);
boolean doSym = "sym".equalsIgnoreCase(kind) || (kind == null && canSym && !canAsym); if (generationKind == GenerationKind.ASYMMETRIC) {
generateAsymmetric(session, store, alg, algId, aliasBase, pubSfx, prvSfx, overwrite);
}
if (generationKind == GenerationKind.SYMMETRIC) {
generateSymmetric(session, store, alg, algId, aliasBase, overwrite);
}
}
if (!doAsym && !doSym && canAsym && canSym) { private static GenerationKind selectGenerationKind(String requestedKind, boolean canAsymmetric,
boolean canSymmetric) {
if ("asym".equalsIgnoreCase(requestedKind) || requestedKind == null && canAsymmetric && !canSymmetric) {
return GenerationKind.ASYMMETRIC;
}
if ("sym".equalsIgnoreCase(requestedKind) || requestedKind == null && canSymmetric && !canAsymmetric) {
return GenerationKind.SYMMETRIC;
}
if (canAsymmetric && canSymmetric) {
throw new IllegalArgumentException("Algorithm supports both; specify --kind sym|asym"); throw new IllegalArgumentException("Algorithm supports both; specify --kind sym|asym");
} }
return GenerationKind.NONE;
}
if (doAsym) { private static void generateAsymmetric(ZeroEchoSession session, KeyringStore store, CryptoAlgorithm algorithm,
KeyPair kp = null; String algorithmId, String aliasBase, String publicSuffix, String privateSuffix, boolean overwrite) {
CryptoAlgorithm.AsymBuilderInfo used = null; GeneratedKeyPair generated = firstGeneratedKeyPair(session, algorithm, algorithmId);
for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) { Class<?> publicImport = findImportSpecClass(algorithm, KeyOperation.ASYMMETRIC_PUBLIC_IMPORT, true);
if (bi.defaultKeySpec == null) { Class<?> privateImport = findImportSpecClass(algorithm, KeyOperation.ASYMMETRIC_PRIVATE_IMPORT, false);
continue; if (publicImport == null && privateImport == null) {
} throw new IllegalStateException("No import spec class found for " + algorithmId + " (asymmetric)");
@SuppressWarnings("unchecked")
Class<AlgorithmKeySpec> st = (Class<AlgorithmKeySpec>) bi.specType;
AsymmetricKeyBuilder<AlgorithmKeySpec> b = alg.asymmetricKeyBuilder(st);
try {
kp = b.generateKeyPair((AlgorithmKeySpec) bi.defaultKeySpec);
if (kp != null) {
used = bi;
break;
}
} catch (Throwable ignore) { // NOPMD
}
}
if (kp == null || used == null) {
throw new IllegalStateException("No asymmetric builder with default spec worked for " + algId);
}
Class<?> pubImp = null;
Class<?> prvImp = null;
for (CryptoAlgorithm.AsymBuilderInfo x : alg.asymmetricBuildersInfo()) {
if (looksLikeImportSpecForPublic(x.specType)) {
pubImp = x.specType;
}
if (looksLikeImportSpecForPrivate(x.specType)) {
prvImp = x.specType;
}
}
if (pubImp == null && prvImp == null) {
throw new IllegalStateException("No import spec class found for " + algId + " (asymmetric)");
}
byte[] spki = kp.getPublic() != null ? kp.getPublic().getEncoded() : null;
byte[] pkcs8 = kp.getPrivate() != null ? kp.getPrivate().getEncoded() : null;
AlgorithmKeySpec pubSpec = pubImp != null ? makeImportSpec(pubImp, spki, algId, used.defaultKeySpec) : null;
AlgorithmKeySpec prvSpec = prvImp != null ? makeImportSpec(prvImp, pkcs8, algId, used.defaultKeySpec)
: null;
if (pubImp != null && pubSpec == null) {
throw new IllegalStateException("Cannot construct public import spec for " + algId);
}
if (prvImp != null && prvSpec == null) {
throw new IllegalStateException("Cannot construct private import spec for " + algId);
}
String pubAlias = aliasBase + pubSfx;
String prvAlias = aliasBase + prvSfx;
ensureWritable(store, pubAlias, overwrite);
ensureWritable(store, prvAlias, overwrite);
store.putPublic(pubAlias, algId, pubSpec);
store.putPrivate(prvAlias, algId, prvSpec);
PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD
out.printf("Generated %s -> %s, %s%n", algId, pubAlias, prvAlias);
} }
if (doSym) { KeyPair pair = generated.pair();
SecretKey sk = null; byte[] publicEncoding = pair.getPublic() == null ? null : pair.getPublic().getEncoded();
CryptoAlgorithm.SymBuilderInfo used = null; byte[] privateEncoding = pair.getPrivate() == null ? null : pair.getPrivate().getEncoded();
for (CryptoAlgorithm.SymBuilderInfo bi : alg.symmetricBuildersInfo()) { AlgorithmKeySpec publicSpec = publicImport == null ? null
if (bi.defaultKeySpec() == null) { : makeImportSpec(publicImport, publicEncoding, algorithmId, generated.info().defaultSpec());
continue; AlgorithmKeySpec privateSpec = privateImport == null ? null
} : makeImportSpec(privateImport, privateEncoding, algorithmId, generated.info().defaultSpec());
@SuppressWarnings("unchecked") requireImportSpec(publicImport, publicSpec, "public", algorithmId);
Class<AlgorithmKeySpec> st = (Class<AlgorithmKeySpec>) bi.specType(); requireImportSpec(privateImport, privateSpec, "private", algorithmId);
SymmetricKeyBuilder<AlgorithmKeySpec> b = alg.symmetricKeyBuilder(st);
try { String publicAlias = aliasBase + publicSuffix;
sk = b.generateSecret((AlgorithmKeySpec) bi.defaultKeySpec()); String privateAlias = aliasBase + privateSuffix;
if (sk != null) { ensureWritable(store, publicAlias, overwrite);
used = bi; ensureWritable(store, privateAlias, overwrite);
break; store.putPublic(publicAlias, algorithmId, publicSpec);
} store.putPrivate(privateAlias, algorithmId, privateSpec);
} catch (Throwable ignore) { // NOPMD
PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD
out.printf("Generated %s -> %s, %s%n", algorithmId, publicAlias, privateAlias);
}
private static GeneratedKeyPair firstGeneratedKeyPair(ZeroEchoSession session, CryptoAlgorithm algorithm,
String algorithmId) {
for (KeyOperationInfo info : algorithm.keyOperations()) {
if (info.operation() != KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE || info.defaultSpec() == null) {
continue;
}
@SuppressWarnings("unchecked")
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) info.specType();
try {
KeyPair pair = session.keyBuilders().asymmetric().keyPairGenerator(algorithmId, specType)
.generateKeyPair(info.defaultSpec());
if (pair != null) {
return new GeneratedKeyPair(pair, info);
} }
} catch (GeneralSecurityException | RuntimeException ignored) { // NOPMD
// Try the next registered default specification.
} }
if (sk == null || used == null) {
throw new IllegalStateException("No symmetric builder with default spec worked for " + algId);
}
Class<?> impSym = findSymmetricImportSpecClass(alg);
if (impSym == null) {
throw new IllegalStateException("No symmetric import spec class for " + algId);
}
byte[] raw = sk.getEncoded();
AlgorithmKeySpec secSpec = makeImportSpec(impSym, raw, algId, used.defaultKeySpec());
if (secSpec == null) {
throw new IllegalStateException("Cannot construct symmetric import spec for " + algId);
}
ensureWritable(store, aliasBase, overwrite);
store.putSecret(aliasBase, algId, secSpec);
PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD
out.printf("Generated %s -> %s%n", algId, aliasBase);
} }
throw new IllegalStateException("No asymmetric builder with default spec worked for " + algorithmId);
}
private static Class<?> findImportSpecClass(CryptoAlgorithm algorithm, KeyOperation operation,
boolean publicImport) {
for (KeyOperationInfo info : algorithm.keyOperations()) {
boolean matchingName = publicImport ? looksLikeImportSpecForPublic(info.specType())
: looksLikeImportSpecForPrivate(info.specType());
if (info.operation() == operation && matchingName) {
return info.specType();
}
}
return null;
}
private static void requireImportSpec(Class<?> specType, AlgorithmKeySpec spec, String kind, String algorithmId) {
if (specType != null && spec == null) {
throw new IllegalStateException("Cannot construct " + kind + " import spec for " + algorithmId);
}
}
private static void generateSymmetric(ZeroEchoSession session, KeyringStore store, CryptoAlgorithm algorithm,
String algorithmId, String alias, boolean overwrite) {
GeneratedSecret generated = firstGeneratedSecret(session, algorithm, algorithmId);
Class<?> importType = findSymmetricImportSpecClass(algorithm);
if (importType == null) {
throw new IllegalStateException("No symmetric import spec class for " + algorithmId);
}
byte[] encoding = generated.key().getEncoded();
AlgorithmKeySpec spec = makeImportSpec(importType, encoding, algorithmId, generated.info().defaultSpec());
if (spec == null) {
throw new IllegalStateException("Cannot construct symmetric import spec for " + algorithmId);
}
ensureWritable(store, alias, overwrite);
store.putSecret(alias, algorithmId, spec);
PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD
out.printf("Generated %s -> %s%n", algorithmId, alias);
}
private static GeneratedSecret firstGeneratedSecret(ZeroEchoSession session, CryptoAlgorithm algorithm,
String algorithmId) {
for (KeyOperationInfo info : algorithm.keyOperations()) {
if (info.operation() != KeyOperation.SYMMETRIC_GENERATE || info.defaultSpec() == null) {
continue;
}
@SuppressWarnings("unchecked")
Class<AlgorithmKeySpec> specType = (Class<AlgorithmKeySpec>) info.specType();
try {
SecretKey key = session.keyBuilders().symmetric().generator(algorithmId, specType)
.generateSecret(info.defaultSpec());
if (key != null) {
return new GeneratedSecret(key, info);
}
} catch (GeneralSecurityException | RuntimeException ignored) { // NOPMD
// Try the next registered default specification.
}
}
throw new IllegalStateException("No symmetric builder with default spec worked for " + algorithmId);
}
private record GeneratedKeyPair(KeyPair pair, KeyOperationInfo info) {
}
private record GeneratedSecret(SecretKey key, KeyOperationInfo info) {
}
/** Selects the exact key-generation operation requested by the command. */
private enum GenerationKind {
ASYMMETRIC,
SYMMETRIC,
NONE
} }
/** /**
@@ -545,19 +590,11 @@ public final class KeyStoreManagement { // NOPMD
} }
private static Class<?> findSymmetricImportSpecClass(CryptoAlgorithm alg) { private static Class<?> findSymmetricImportSpecClass(CryptoAlgorithm alg) {
for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) { for (KeyOperationInfo x : alg.keyOperations()) {
String n = x.specType().getSimpleName(); if (x.operation() == KeyOperation.SYMMETRIC_IMPORT) {
if (n.contains("Import") || n.endsWith("KeyImportSpec") || n.endsWith("SecretSpec")) {
return x.specType(); return x.specType();
} }
} }
for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) {
try {
x.specType().getConstructor(byte[].class);
return x.specType();
} catch (NoSuchMethodException ignored) {
}
}
return null; return null;
} }

View File

@@ -59,6 +59,7 @@ import zeroecho.core.spec.VoidSpec;
import zeroecho.core.storage.KeyringStore; import zeroecho.core.storage.KeyringStore;
import zeroecho.core.tag.TagEngineBuilder; import zeroecho.core.tag.TagEngineBuilder;
import zeroecho.sdk.builders.TagTrailerDataContentBuilder; import zeroecho.sdk.builders.TagTrailerDataContentBuilder;
import zeroecho.sdk.ZeroEchoSession;
import zeroecho.sdk.content.api.DataContent; import zeroecho.sdk.content.api.DataContent;
import zeroecho.sdk.content.api.PlainContent; import zeroecho.sdk.content.api.PlainContent;
import zeroecho.sdk.content.builtin.PlainFile; import zeroecho.sdk.content.builtin.PlainFile;
@@ -157,6 +158,7 @@ public final class Tag { // NOPMD
* signature or digest processing * signature or digest processing
*/ */
public static int main(String[] args, Options root) throws ParseException, IOException, GeneralSecurityException { public static int main(String[] args, Options root) throws ParseException, IOException, GeneralSecurityException {
ZeroEchoSession session = new ZeroEchoSession();
Options opts = root; Options opts = root;
opts.addOption(TYPE_OPT); opts.addOption(TYPE_OPT);
opts.addOption(MODE_OPT); opts.addOption(MODE_OPT);
@@ -196,21 +198,23 @@ public final class Tag { // NOPMD
if (TYPE_SIGNATURE.equals(type)) { if (TYPE_SIGNATURE.equals(type)) {
String ksPath = require(cli, KS_OPT, "--ks <file> is required for --type signature"); String ksPath = require(cli, KS_OPT, "--ks <file> is required for --type signature");
KeyringStore keyring = KeyringStore.load(Path.of(ksPath)); KeyringStore keyring = KeyringStore.load(session, Path.of(ksPath));
ContextSpec spec = VoidSpec.INSTANCE; ContextSpec spec = VoidSpec.INSTANCE;
if (produce) { if (produce) {
String privAlias = require(cli, PRIV_OPT, "signature produce requires --priv <alias>"); String privAlias = require(cli, PRIV_OPT, "signature produce requires --priv <alias>");
PrivateKey priv = keyring.getPrivate(privAlias); PrivateKey priv = keyring.getPrivate(privAlias);
tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(alg, priv, spec)).build(true); tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(session, alg, priv, spec))
.build(true);
} else { } else {
String pubAlias = require(cli, PUB_OPT, "signature verify requires --pub <alias>"); String pubAlias = require(cli, PUB_OPT, "signature verify requires --pub <alias>");
PublicKey pub = keyring.getPublic(pubAlias); PublicKey pub = keyring.getPublic(pubAlias);
tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(alg, pub, spec)).build(false); tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(session, alg, pub, spec))
.build(false);
} }
} else { // digest } else { // digest
DigestSpec spec = parseDigest(alg); DigestSpec spec = parseDigest(alg);
tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(spec)).build(produce); tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(session, spec)).build(produce);
} }
tail.setInput(source); tail.setInput(source);

View File

@@ -36,6 +36,7 @@ package zeroecho;
import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.PrintStream; import java.io.PrintStream;
import java.nio.file.Files; import java.nio.file.Files;
@@ -45,6 +46,7 @@ import java.util.Arrays;
import java.util.Random; import java.util.Random;
import org.apache.commons.cli.Options; import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -74,6 +76,7 @@ import zeroecho.sdk.util.BouncyCastleActivator;
* </p> * </p>
*/ */
public class GuardTest { public class GuardTest {
private static final String TEST_PBKDF2_MAXIMUM = "1000000";
/** All temporary files live here and are auto-cleaned by JUnit. */ /** All temporary files live here and are auto-cleaned by JUnit. */
@TempDir @TempDir
@@ -120,14 +123,16 @@ public class GuardTest {
Path dec = tmp.resolve("pt.bin.dec"); Path dec = tmp.resolve("pt.bin.dec");
// Encrypt // Encrypt
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", password, "--alg", String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", password,
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg",
"aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aadHex }; "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aadHex };
System.out.println("...encrypt: " + Arrays.toString(encArgs)); System.out.println("...encrypt: " + Arrays.toString(encArgs));
int e = Guard.main(encArgs, new Options()); int e = Guard.main(encArgs, new Options());
assertEquals(0, e, "... encrypt expected exit code 0"); assertEquals(0, e, "... encrypt expected exit code 0");
// Decrypt (using password) // Decrypt (using password)
String[] decArgs = { "--decrypt", enc.toString(), "--output", dec.toString(), "--password", password, "--alg", String[] decArgs = { "--decrypt", enc.toString(), "--output", dec.toString(), "--password", password,
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg",
"aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aadHex }; "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aadHex };
System.out.println("...decrypt: " + Arrays.toString(decArgs)); System.out.println("...decrypt: " + Arrays.toString(decArgs));
int d = Guard.main(decArgs, new Options()); int d = Guard.main(decArgs, new Options());
@@ -137,6 +142,42 @@ public class GuardTest {
System.out.println("...ok"); System.out.println("...ok");
} }
@Test
void passwordOperationRequiresExplicitLimits() throws Exception {
String method = "passwordOperationRequiresExplicitLimits";
System.out.println(method);
Path input = writeRandom(tmp.resolve("limits.bin"), 32, 0x51A17);
String[] arguments = { "--encrypt", input.toString(), "--to-psw", "controlled", "--alg", "aes-gcm" };
ParseException failure = assertThrows(ParseException.class,
() -> Guard.main(arguments, new Options()));
assertTrue(failure.getMessage().contains("--pbkdf2-max"));
System.out.println("...rejected=missingLimits");
System.out.println(method + "...ok");
}
@Test
void recipientKekOptionRejectsUnreadableSize() throws Exception {
String method = "recipientKekOptionRejectsUnreadableSize";
System.out.println(method);
Path input = writeRandom(tmp.resolve("invalid-kek.bin"), 32, 0x4B454B);
Path output = tmp.resolve("invalid-kek.enc");
String[] arguments = { "--encrypt", input.toString(), "--output", output.toString(),
"--to-psw", "controlled", "--to-kek-bytes", "24",
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM,
"--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM,
"--alg", "aes-gcm" };
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
() -> Guard.main(arguments, new Options()));
assertTrue(failure.getMessage().contains("exactly 16 or 32"));
assertTrue(Files.notExists(output));
System.out.println("...rejectedKekBytes=24");
System.out.println(method + "...ok");
}
/** /**
* RSA recipient round trips with both AES-GCM and ChaCha20-Poly1305 payloads. * RSA recipient round trips with both AES-GCM and ChaCha20-Poly1305 payloads.
* *
@@ -249,7 +290,8 @@ public class GuardTest {
// alias, // alias,
// plus 2 random password decoys. Recipients are shuffled by default. // plus 2 random password decoys. Recipients are shuffled by default.
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--keyring", ring.toString(), String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--keyring", ring.toString(),
"--to-alias", rsa.pub, "--to-psw", password, "--decoy-alias", elg.pub, "--decoy-psw-rand", "2", "--alg", "--to-alias", rsa.pub, "--to-psw", password, "--decoy-alias", elg.pub, "--decoy-psw-rand", "2",
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg",
"aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aad }; "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aad };
System.out.println("...encrypt: " + Arrays.toString(encArgs)); System.out.println("...encrypt: " + Arrays.toString(encArgs));
int e = Guard.main(encArgs, new Options()); int e = Guard.main(encArgs, new Options());
@@ -266,7 +308,8 @@ public class GuardTest {
"mixed recipients decrypt(private) mismatch"); "mixed recipients decrypt(private) mismatch");
// Decrypt via password instead of key // Decrypt via password instead of key
String[] decPwd = { "--decrypt", enc.toString(), "--output", dec2.toString(), "--password", password, "--alg", String[] decPwd = { "--decrypt", enc.toString(), "--output", dec2.toString(), "--password", password,
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg",
"aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aad }; "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aad };
System.out.println("...decrypt(password): " + Arrays.toString(decPwd)); System.out.println("...decrypt(password): " + Arrays.toString(decPwd));
int d2 = Guard.main(decPwd, new Options()); int d2 = Guard.main(decPwd, new Options());
@@ -292,7 +335,8 @@ public class GuardTest {
Path enc = tmp.resolve("pt-neg.bin.enc"); Path enc = tmp.resolve("pt-neg.bin.enc");
String pwd = "x"; String pwd = "x";
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", pwd, "--alg", String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", pwd,
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg",
"aes-gcm", "--tag-bits", "128" }; "aes-gcm", "--tag-bits", "128" };
int e = Guard.main(encArgs, new Options()); int e = Guard.main(encArgs, new Options());
assertEquals(0, e, "... encrypt rc"); assertEquals(0, e, "... encrypt rc");

View File

@@ -166,7 +166,7 @@ public class KemTest {
KeyAliases aliases = generateKemIntoKeyStore(ring, kemId, "alias-" + shortId(kemId)); KeyAliases aliases = generateKemIntoKeyStore(ring, kemId, "alias-" + shortId(kemId));
// Sanity: re-open to ensure the file is valid // Sanity: re-open to ensure the file is valid
KeyringStore ks = KeyringStore.load(ring); KeyringStore ks = KeyringStore.load(new zeroecho.sdk.ZeroEchoSession(), ring);
if (!(ks.contains(aliases.pub) && ks.contains(aliases.prv))) { if (!(ks.contains(aliases.pub) && ks.contains(aliases.prv))) {
throw new IllegalStateException("Keyring does not contain expected aliases for " + kemId); throw new IllegalStateException("Keyring does not contain expected aliases for " + kemId);
} }

View File

@@ -52,10 +52,9 @@ import org.junit.jupiter.api.io.TempDir;
import zeroecho.core.CryptoAlgorithm; import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.CryptoAlgorithms; import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.KeyOperation;
import zeroecho.core.spi.AsymmetricKeyBuilder;
import zeroecho.core.spi.SymmetricKeyBuilder;
import zeroecho.core.storage.KeyringStore; import zeroecho.core.storage.KeyringStore;
import zeroecho.sdk.ZeroEchoSession;
import zeroecho.sdk.util.BouncyCastleActivator; import zeroecho.sdk.util.BouncyCastleActivator;
/** /**
@@ -91,6 +90,7 @@ public class KeyStoreManagementTest {
@Test @Test
public void generateAndVerifyAllAlgorithms() throws Exception { public void generateAndVerifyAllAlgorithms() throws Exception {
Path ring = tmp.resolve("ring.txt"); Path ring = tmp.resolve("ring.txt");
ZeroEchoSession session = new ZeroEchoSession();
Set<String> algIds = CryptoAlgorithms.available(); Set<String> algIds = CryptoAlgorithms.available();
System.out.println("Algorithms: " + algIds); System.out.println("Algorithms: " + algIds);
@@ -139,7 +139,7 @@ public class KeyStoreManagementTest {
assertTrue(attempted > 0, "No generation attempts were successful"); assertTrue(attempted > 0, "No generation attempts were successful");
// Verify by reloading and materializing. // Verify by reloading and materializing.
KeyringStore store = KeyringStore.load(ring); KeyringStore store = KeyringStore.load(session, ring);
List<String> aliases = store.aliases(); List<String> aliases = store.aliases();
System.out.println("Reloaded aliases (" + aliases.size() + "): " + aliases); System.out.println("Reloaded aliases (" + aliases.size() + "): " + aliases);
@@ -189,45 +189,15 @@ public class KeyStoreManagementTest {
// ---- helpers ---- // ---- helpers ----
private static boolean hasAsymmetricDefault(CryptoAlgorithm alg) { private static boolean hasAsymmetricDefault(CryptoAlgorithm alg) {
try { return alg.keyOperations().stream()
List<CryptoAlgorithm.AsymBuilderInfo> infos = alg.asymmetricBuildersInfo(); .anyMatch(info -> info.operation() == KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE
for (int i = 0; i < infos.size(); i++) { && info.defaultSpec() != null);
CryptoAlgorithm.AsymBuilderInfo bi = infos.get(i);
if (bi.defaultKeySpec == null) {
continue;
}
@SuppressWarnings("unchecked")
Class<AlgorithmKeySpec> st = (Class<AlgorithmKeySpec>) bi.specType;
AsymmetricKeyBuilder<AlgorithmKeySpec> b = alg.asymmetricKeyBuilder(st);
if (b != null) {
return true;
}
}
} catch (Throwable t) {
return false;
}
return false;
} }
private static boolean hasSymmetricDefault(CryptoAlgorithm alg) { private static boolean hasSymmetricDefault(CryptoAlgorithm alg) {
try { return alg.keyOperations().stream()
List<CryptoAlgorithm.SymBuilderInfo> infos = alg.symmetricBuildersInfo(); .anyMatch(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE
for (int i = 0; i < infos.size(); i++) { && info.defaultSpec() != null);
CryptoAlgorithm.SymBuilderInfo bi = infos.get(i);
if (bi.defaultKeySpec() == null) {
continue;
}
@SuppressWarnings("unchecked")
Class<AlgorithmKeySpec> st = (Class<AlgorithmKeySpec>) bi.specType();
SymmetricKeyBuilder<AlgorithmKeySpec> b = alg.symmetricKeyBuilder(st);
if (b != null) {
return true;
}
}
} catch (Throwable t) {
return false;
}
return false;
} }
private static String sanitize(String id) { private static String sanitize(String id) {

View File

@@ -127,7 +127,7 @@ public class TagTest {
Path ring = tmp.resolve("ring-ed25519.txt"); Path ring = tmp.resolve("ring-ed25519.txt");
KeyAliases ed = generateIntoKeyStore(ring, "Ed25519", "ed"); KeyAliases ed = generateIntoKeyStore(ring, "Ed25519", "ed");
// sanity // sanity
KeyringStore ks = KeyringStore.load(ring); KeyringStore ks = KeyringStore.load(new zeroecho.sdk.ZeroEchoSession(), ring);
assertTrue(ks.contains(ed.pub) && ks.contains(ed.prv), "missing expected aliases"); assertTrue(ks.contains(ed.pub) && ks.contains(ed.prv), "missing expected aliases");
byte[] pt = randomBytes(4096); byte[] pt = randomBytes(4096);

View File

@@ -55,13 +55,13 @@ import org.junit.jupiter.api.io.TempDir;
import conflux.Ctx; import conflux.Ctx;
import conflux.CtxInterface; import conflux.CtxInterface;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.alg.aes.AesKeyGenSpec; import zeroecho.core.alg.aes.AesKeyGenSpec;
import zeroecho.core.alg.aes.AesSpec; import zeroecho.core.alg.aes.AesSpec;
import zeroecho.sdk.builders.alg.AesDataContentBuilder; import zeroecho.sdk.builders.alg.AesDataContentBuilder;
import zeroecho.sdk.builders.core.DataContentChainBuilder; import zeroecho.sdk.builders.core.DataContentChainBuilder;
import zeroecho.sdk.builders.core.PlainBytesBuilder; import zeroecho.sdk.builders.core.PlainBytesBuilder;
import zeroecho.sdk.content.api.DataContent; import zeroecho.sdk.content.api.DataContent;
import zeroecho.sdk.ZeroEchoSession;
class JpegExifIntegrationTest { class JpegExifIntegrationTest {
@@ -91,17 +91,17 @@ class JpegExifIntegrationTest {
// AES encryption setup // AES encryption setup
/* /*
* CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); SecretKey key = * SecretKey key = zeroEchoSession.keyBuilders().symmetric()
* aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec. * .generate("AES", AesKeyGenSpec.aes256()); AesSpec spec =
* aes256()); AesSpec spec =
* AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build(); * AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build();
* EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, * EncryptionContext enc = zeroEchoSession.createContext("AES", KeyUsage.ENCRYPT, key,
* spec); CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + * spec); CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" +
* System.nanoTime()); session.put(ConfluxKeys.aad("AES"), aad); ((ContextAware) * System.nanoTime()); session.put(ConfluxKeys.aad("AES"), aad); ((ContextAware)
* enc).setContext(session); * enc).setContext(session);
*/ */
SecretKey key = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class) ZeroEchoSession zeroEchoSession = new ZeroEchoSession();
.generateSecret(AesKeyGenSpec.aes256()); SecretKey key = zeroEchoSession.keyBuilders().symmetric()
.generate("AES", AesKeyGenSpec.aes256());
CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime()); CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime());
byte[] encryptedBytes; byte[] encryptedBytes;
@@ -109,7 +109,7 @@ class JpegExifIntegrationTest {
// input // input
.add(PlainBytesBuilder.builder().bytes(inputBytes)) .add(PlainBytesBuilder.builder().bytes(inputBytes))
// encryption // encryption
.add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()) .add(AesDataContentBuilder.builder(zeroEchoSession).importKeyRaw(key.getEncoded())
// using general AES/GCM/128 without specified header // using general AES/GCM/128 without specified header
.spec(AesSpec.gcm128(null)) .spec(AesSpec.gcm128(null))
// but let the builder add the default header for storing AAD and IV // but let the builder add the default header for storing AAD and IV
@@ -152,7 +152,7 @@ class JpegExifIntegrationTest {
// input // input
.add(PlainBytesBuilder.builder().bytes(extractedEncryptedBytes)) .add(PlainBytesBuilder.builder().bytes(extractedEncryptedBytes))
// encryption // encryption
.add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()).spec(AesSpec.gcm128(null)) .add(AesDataContentBuilder.builder(zeroEchoSession).importKeyRaw(key.getEncoded()).spec(AesSpec.gcm128(null))
// let us use the default header for AAD and IV // let us use the default header for AAD and IV
.withHeader().withAad(aad).context(session)) .withHeader().withAad(aad).context(session))
// and create the pipeline // and create the pipeline
@@ -164,7 +164,7 @@ class JpegExifIntegrationTest {
/* /*
* AesSpec spec = * AesSpec spec =
* AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build(); * AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build();
* EncryptionContext dec1 = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, * EncryptionContext dec1 = zeroEchoSession.createContext("AES", KeyUsage.DECRYPT,
* key, spec); ((ContextAware) dec1).setContext(session); // same IV/AAD in ctx * key, spec); ((ContextAware) dec1).setContext(session); // same IV/AAD in ctx
* byte[] pt1 = readAll(dec1.attach(new * byte[] pt1 = readAll(dec1.attach(new
* ByteArrayInputStream(extractedEncryptedBytes))); dec1.close(); * ByteArrayInputStream(extractedEncryptedBytes))); dec1.close();

View File

@@ -3,103 +3,53 @@
* All rights reserved. * All rights reserved.
* *
* Redistribution and use in source and binary forms, with or without modification, * Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met: * are permitted provided that the conditions in the project LICENSE 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; package zeroecho.core;
import java.security.Key; import java.security.Key;
import java.util.Objects; import java.util.Objects;
import java.util.function.Supplier;
import zeroecho.core.alg.AbstractCryptoAlgorithm;
import zeroecho.core.context.CryptoContext; import zeroecho.core.context.CryptoContext;
import zeroecho.core.spec.ContextSpec; import zeroecho.core.spec.ContextSpec;
import zeroecho.core.spi.ContextConstructorKS;
/** /**
* Immutable descriptor of an algorithm capability. * Immutable value descriptor of one algorithm context capability.
* *
* <p> * <p>The default specification is resolved once during provider construction.
* A {@code Capability} describes one role supported by a * All components therefore have stable value semantics and are safe for
* {@link CryptoAlgorithm}, including: * concurrent reads.</p>
* </p>
* <ul>
* <li>the algorithm identifier,</li>
* <li>its high-level {@link AlgorithmFamily},</li>
* <li>the {@link KeyUsage} role (e.g., ENCRYPT, VERIFY),</li>
* <li>the expected {@link CryptoContext} type,</li>
* <li>the accepted {@link Key} type,</li>
* <li>the accepted {@link ContextSpec} type, and</li>
* <li>a supplier for a default spec.</li>
* </ul>
*
* <h2>Purpose</h2> Capabilities allow discovery, inspection, and documentation
* of what an algorithm can do. Higher layers (e.g., protocol builders,
* registries, tooling) can enumerate capabilities via
* {@link CryptoAlgorithm#listCapabilities()} and adapt automatically.
*
* <p>
* Each capability corresponds to a call to
* {@link AbstractCryptoAlgorithm#capability(AlgorithmFamily, KeyUsage, Class, Class, Class, ContextConstructorKS, Supplier)}.
* </p>
*
* <h2>Thread-safety</h2> {@code Capability} instances are immutable and safe to
* share across threads.
* *
* @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 * @since 1.0
*/ */
public record Capability(String algorithmId, AlgorithmFamily family, KeyUsage role, public record Capability(String algorithmId, AlgorithmFamily family, KeyUsage role,
Class<? extends CryptoContext> contextType, Class<? extends Key> keyType, Class<? extends ContextSpec> specType, Class<? extends CryptoContext> contextType, Class<? extends Key> keyType,
Supplier<? extends ContextSpec> defaultSpec) { 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 * @throws NullPointerException if a component is {@code null}
* @param family high-level algorithm family classification * @throws IllegalArgumentException if {@code defaultSpec} is incompatible
* @param role supported {@link KeyUsage} role * with {@code specType}
* @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}
*/ */
public Capability(String algorithmId, AlgorithmFamily family, KeyUsage role, public Capability {
Class<? extends CryptoContext> contextType, Class<? extends Key> keyType, Objects.requireNonNull(algorithmId, "algorithmId must not be null");
Class<? extends ContextSpec> specType, Supplier<? extends ContextSpec> defaultSpec) { Objects.requireNonNull(family, "family must not be null");
this.algorithmId = Objects.requireNonNull(algorithmId, "algorithmId must not be null"); Objects.requireNonNull(role, "role must not be null");
this.family = Objects.requireNonNull(family, "family must not be null"); Objects.requireNonNull(contextType, "contextType must not be null");
this.role = Objects.requireNonNull(role, "role must not be null"); Objects.requireNonNull(keyType, "keyType must not be null");
this.contextType = Objects.requireNonNull(contextType, "contextType must not be null"); Objects.requireNonNull(specType, "specType must not be null");
this.keyType = Objects.requireNonNull(keyType, "keyType must not be null"); Objects.requireNonNull(defaultSpec, "defaultSpec must not be null");
this.specType = Objects.requireNonNull(specType, "specType must not be null"); if (!specType.isInstance(defaultSpec)) {
this.defaultSpec = Objects.requireNonNull(defaultSpec, "defaultSpec must not be null"); throw new IllegalArgumentException("defaultSpec must be an instance of " + specType.getName());
}
} }
} }

View File

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

View File

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

View File

@@ -33,10 +33,7 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.core; package zeroecho.core;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.ServiceLoader;
import zeroecho.core.annotation.Describable; import zeroecho.core.annotation.Describable;
import zeroecho.core.annotation.DisplayName; import zeroecho.core.annotation.DisplayName;
@@ -47,8 +44,8 @@ import zeroecho.core.annotation.DisplayName;
* *
* <p> * <p>
* {@code CryptoCatalog} is a lightweight registry built at a point in time via * {@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 #load()}. It consumes the authoritative provider registry owned by
* {@link CryptoAlgorithm}, ensures identifier uniqueness, and exposes: * {@link CryptoAlgorithms} and exposes:
* </p> * </p>
* *
* <ul> * <ul>
@@ -60,8 +57,8 @@ import zeroecho.core.annotation.DisplayName;
* </ul> * </ul>
* *
* <h2>Identity and uniqueness</h2> Algorithm ids are treated as canonical keys. * <h2>Identity and uniqueness</h2> Algorithm ids are treated as canonical keys.
* If two providers expose the same {@linkplain CryptoAlgorithm#id() id}, the * Duplicate provider identifiers are rejected when the authoritative registry
* catalog build fails with {@link IllegalStateException}. * is initialized.
* *
* <h2>Immutability &amp; thread-safety</h2> After construction, the internal * <h2>Immutability &amp; thread-safety</h2> After construction, the internal
* map is unmodifiable and safe to share across threads. This class performs no * map is unmodifiable and safe to share across threads. This class performs no
@@ -77,10 +74,9 @@ import zeroecho.core.annotation.DisplayName;
* </ul> * </ul>
* *
* <p> * <p>
* <b>Note:</b> Default spec / key-spec values shown in outputs are derived from * <b>Note:</b> Default spec / key-spec values shown in outputs are stable
* {@code Supplier}s registered by algorithms. Suppliers may compute labels or * metadata values resolved when providers are initialized; their intent is
* return lightweight descriptors; their intent is documentation, not * documentation, not round-tripping.
* roundtripping.
* </p> * </p>
* *
* @since 1.0 * @since 1.0
@@ -93,26 +89,25 @@ public final class CryptoCatalog {
} }
/** /**
* Discovers {@link CryptoAlgorithm} implementations via {@link ServiceLoader} * Returns a catalog view of the authoritative registry initialized by
* and returns an immutable catalog snapshot. * {@link CryptoAlgorithms}.
* *
* <p> * <p>
* During loading, algorithm ids are checked for uniqueness. A duplicate id * Provider discovery, deterministic ordering, and duplicate checking occur
* results in an {@link IllegalStateException} to prevent ambiguous resolution. * once in {@code CryptoAlgorithms}. This method neither scans providers nor
* copies their collection.
* </p> * </p>
* *
* @return an immutable {@code CryptoCatalog} with all discovered algorithms * @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() { public static CryptoCatalog load() {
Map<String, CryptoAlgorithm> m = new HashMap<>(); return new CryptoCatalog(CryptoAlgorithms.registry());
ServiceLoader.load(CryptoAlgorithm.class).forEach(a -> { }
CryptoAlgorithm prev = m.put(a.id(), a);
if (prev != null) { /* default */ Map<String, CryptoAlgorithm> algorithms() {
throw new IllegalStateException("Duplicate algorithm id: " + a.id()); return algos;
}
});
return new CryptoCatalog(Collections.unmodifiableMap(m));
} }
/** /**
@@ -132,9 +127,8 @@ public final class CryptoCatalog {
StringBuilder sb = null; StringBuilder sb = null;
for (CryptoAlgorithm a : algos.values()) { for (CryptoAlgorithm a : algos.values()) {
boolean hasCaps = !a.listCapabilities().isEmpty(); boolean hasCaps = !a.listCapabilities().isEmpty();
boolean hasAsym = !a.asymmetricBuildersInfo().isEmpty(); boolean hasKeyOperations = !a.keyOperations().isEmpty();
boolean hasSym = !a.symmetricBuildersInfo().isEmpty(); if (!hasCaps && !hasKeyOperations) {
if (!hasCaps && !hasAsym && !hasSym) {
if (sb == null) { if (sb == null) {
sb = new StringBuilder(50 /* minimal record size */ * 6 /* suggested avg of error records */); // NOPMD sb = new StringBuilder(50 /* minimal record size */ * 6 /* suggested avg of error records */); // NOPMD
} }
@@ -225,30 +219,20 @@ public final class CryptoCatalog {
.append(jsonField("contextType", cap.contextType().getSimpleName())).append(',') .append(jsonField("contextType", cap.contextType().getSimpleName())).append(',')
.append(jsonField("keyType", cap.keyType().getSimpleName())).append(',') .append(jsonField("keyType", cap.keyType().getSimpleName())).append(',')
.append(jsonField("specType", cap.specType().getSimpleName())).append(",\"defaultSpec\":") .append(jsonField("specType", cap.specType().getSimpleName())).append(",\"defaultSpec\":")
.append(cap.defaultSpec() == null ? "null" : jsonString(labelOf(cap.defaultSpec().get()))) .append(cap.defaultSpec() == null ? "null" : jsonString(labelOf(cap.defaultSpec())))
.append('}'); .append('}');
} }
sb.append("],\"asymmetricKeyBuilders\":["); sb.append("],\"keyOperations\":[");
boolean fa = true; boolean firstOperation = true;
for (CryptoAlgorithm.AsymBuilderInfo kb : a.asymmetricBuildersInfo()) { for (KeyOperationInfo operation : a.keyOperations()) {
if (!fa) { if (!firstOperation) {
sb.append(','); sb.append(',');
} }
fa = false; firstOperation = false;
sb.append('{').append(jsonField("specType", kb.specType.getSimpleName())).append(",\"defaultKeySpec\":") sb.append('{').append(jsonField("operation", operation.operation().name())).append(',')
.append(kb.defaultKeySpec == null ? "null" : jsonString(labelOf(kb.defaultKeySpec))) .append(jsonField("specType", operation.specType().getSimpleName()))
.append('}'); .append(",\"defaultSpec\":")
} .append(operation.defaultSpec() == null ? "null" : jsonString(labelOf(operation.defaultSpec())))
sb.append("],\"symmetricKeyBuilders\":[");
boolean fs = true;
for (CryptoAlgorithm.SymBuilderInfo kb : a.symmetricBuildersInfo()) {
if (!fs) {
sb.append(',');
}
fs = false;
sb.append('{').append(jsonField("specType", kb.specType().getSimpleName()))
.append(",\"defaultKeySpec\":")
.append(kb.defaultKeySpec() == null ? "null" : jsonString(labelOf(kb.defaultKeySpec())))
.append('}'); .append('}');
} }
sb.append("]}"); sb.append("]}");
@@ -285,23 +269,17 @@ public final class CryptoCatalog {
.append(esc(cap.contextType().getSimpleName())).append("</contextType><keyType>") .append(esc(cap.contextType().getSimpleName())).append("</contextType><keyType>")
.append(esc(cap.keyType().getSimpleName())).append("</keyType><specType>") .append(esc(cap.keyType().getSimpleName())).append("</keyType><specType>")
.append(esc(cap.specType().getSimpleName())).append("</specType><defaultSpec>") .append(esc(cap.specType().getSimpleName())).append("</specType><defaultSpec>")
.append(esc(labelOf(cap.defaultSpec().get()))).append("</defaultSpec></capability>"); .append(esc(labelOf(cap.defaultSpec()))).append("</defaultSpec></capability>");
} }
sb.append("</capabilities><asymmetricKeyBuilders>"); sb.append("</capabilities><keyOperations>");
for (CryptoAlgorithm.AsymBuilderInfo kb : a.asymmetricBuildersInfo()) { for (KeyOperationInfo operation : a.keyOperations()) {
sb.append("<keyBuilder specType=\"").append(esc(kb.specType.getSimpleName())) sb.append("<keyOperation operation=\"").append(operation.operation().name())
.append("\"><defaultKeySpec>") .append("\" specType=\"").append(esc(operation.specType().getSimpleName()))
.append(kb.defaultKeySpec == null ? "" : esc(labelOf(kb.defaultKeySpec))) .append("\"><defaultSpec>")
.append("</defaultKeySpec></keyBuilder>"); .append(operation.defaultSpec() == null ? "" : esc(labelOf(operation.defaultSpec())))
.append("</defaultSpec></keyOperation>");
} }
sb.append("</asymmetricKeyBuilders><symmetricKeyBuilders>"); sb.append("</keyOperations></algorithm>");
for (CryptoAlgorithm.SymBuilderInfo kb : a.symmetricBuildersInfo()) {
sb.append("<keyBuilder specType=\"").append(esc(kb.specType().getSimpleName()))
.append("\"><defaultKeySpec>")
.append(kb.defaultKeySpec() == null ? "" : esc(labelOf(kb.defaultKeySpec())))
.append("</defaultKeySpec></keyBuilder>");
}
sb.append("</symmetricKeyBuilders></algorithm>");
} }
sb.append("</cryptoCatalog>"); sb.append("</cryptoCatalog>");
return sb.toString(); return sb.toString();

View File

@@ -0,0 +1,26 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the conditions in the project LICENSE are met.
******************************************************************************/
package zeroecho.core;
/**
* Identifies one exact key-material operation exposed by an algorithm.
*
* @since 1.0
*/
public enum KeyOperation {
/** Generates a symmetric key. */
SYMMETRIC_GENERATE,
/** Imports a symmetric key. */
SYMMETRIC_IMPORT,
/** Generates an asymmetric key pair. */
ASYMMETRIC_KEY_PAIR_GENERATE,
/** Imports an asymmetric public key. */
ASYMMETRIC_PUBLIC_IMPORT,
/** Imports an asymmetric private key. */
ASYMMETRIC_PRIVATE_IMPORT
}

View File

@@ -0,0 +1,47 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the conditions in the project LICENSE are met.
******************************************************************************/
package zeroecho.core;
import java.util.Objects;
import zeroecho.core.spec.AlgorithmKeySpec;
/**
* Immutable metadata for one exact key operation.
*
* @param operation operation guaranteed by the associated lookup
* @param specType exact accepted specification type
* @param defaultSpec resolved generation default, or {@code null} for import
* operations and generators without a default
* @since 1.0
*/
public record KeyOperationInfo(KeyOperation operation,
Class<? extends AlgorithmKeySpec> specType, AlgorithmKeySpec defaultSpec) {
/**
* Validates the metadata invariant.
*
* @throws NullPointerException if {@code operation} or {@code specType} is
* {@code null}
* @throws IllegalArgumentException if a default is incompatible with
* {@code specType}, or an import operation
* declares a default
*/
public KeyOperationInfo {
Objects.requireNonNull(operation, "operation must not be null");
Objects.requireNonNull(specType, "specType must not be null");
if (defaultSpec != null && !specType.isInstance(defaultSpec)) {
throw new IllegalArgumentException("defaultSpec must be an instance of " + specType.getName());
}
if (defaultSpec != null && (operation == KeyOperation.SYMMETRIC_IMPORT
|| operation == KeyOperation.ASYMMETRIC_PUBLIC_IMPORT
|| operation == KeyOperation.ASYMMETRIC_PRIVATE_IMPORT)) {
throw new IllegalArgumentException("import operations cannot declare a default specification");
}
}
}

View File

@@ -34,6 +34,7 @@
package zeroecho.core.alg; package zeroecho.core.alg;
import java.security.Key; import java.security.Key;
import java.util.Objects;
import java.util.function.Supplier; import java.util.function.Supplier;
import zeroecho.core.AlgorithmFamily; import zeroecho.core.AlgorithmFamily;
@@ -42,7 +43,7 @@ import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.KeyUsage; import zeroecho.core.KeyUsage;
import zeroecho.core.context.CryptoContext; import zeroecho.core.context.CryptoContext;
import zeroecho.core.spec.ContextSpec; import zeroecho.core.spec.ContextSpec;
import zeroecho.core.spi.ContextConstructorKS; import zeroecho.core.spi.ContextFactoryKS;
/** /**
* Convenience base class for concrete {@link CryptoAlgorithm} implementations. * Convenience base class for concrete {@link CryptoAlgorithm} implementations.
@@ -54,7 +55,7 @@ import zeroecho.core.spi.ContextConstructorKS;
* *
* <ol> * <ol>
* <li><b>Binding roles to runtime factories</b> via * <li><b>Binding roles to runtime factories</b> via
* {@link #capability(AlgorithmFamily, KeyUsage, Class, Class, Class, ContextConstructorKS, Supplier)}, * {@link #capability(AlgorithmFamily, KeyUsage, Class, Class, Class, ContextFactoryKS, Supplier)},
* which registers a {@link KeyUsage role} together with its expected * which registers a {@link KeyUsage role} together with its expected
* {@link CryptoContext} type, accepted {@link Key} type, optional * {@link CryptoContext} type, accepted {@link Key} type, optional
* {@link ContextSpec} type, the constructor factory, and a default spec * {@link ContextSpec} type, the constructor factory, and a default spec
@@ -134,8 +135,8 @@ public abstract class AbstractCryptoAlgorithm extends CryptoAlgorithm {
* </p> * </p>
* <ul> * <ul>
* <li><b>Runtime binding:</b> delegates to * <li><b>Runtime binding:</b> delegates to
* {@link CryptoAlgorithm#bind(KeyUsage, Class, Class, Class, ContextConstructorKS, Supplier)} * {@link CryptoAlgorithm#bindContext(KeyUsage, Class, Class, Class, ContextFactoryKS, Supplier)}
* so that {@link CryptoAlgorithm#create(KeyUsage, Key, ContextSpec)} can * so that {@link CryptoAlgorithm#createContext(KeyUsage, Key, ContextSpec)} can
* construct the appropriate {@link CryptoContext} when invoked.</li> * construct the appropriate {@link CryptoContext} when invoked.</li>
* <li><b>Metadata publication:</b> creates a {@link Capability} describing this * <li><b>Metadata publication:</b> creates a {@link Capability} describing this
* role (algorithm id, {@link AlgorithmFamily family}, role, context/key/spec * role (algorithm id, {@link AlgorithmFamily family}, role, context/key/spec
@@ -145,7 +146,8 @@ public abstract class AbstractCryptoAlgorithm extends CryptoAlgorithm {
* </ul> * </ul>
* *
* <h4>Validation</h4> Type checks happen at creation time (via {@code bind}) * <h4>Validation</h4> Type checks happen at creation time (via {@code bind})
* and again when {@link CryptoAlgorithm#create(KeyUsage, Key, ContextSpec)} is * and again when
* {@link CryptoAlgorithm#createContext(KeyUsage, Key, ContextSpec)} is
* called. If a factory returns a context not assignable to {@code ctxType}, an * called. If a factory returns a context not assignable to {@code ctxType}, an
* {@link IllegalStateException} will be thrown. * {@link IllegalStateException} will be thrown.
* *
@@ -157,21 +159,24 @@ public abstract class AbstractCryptoAlgorithm extends CryptoAlgorithm {
* @param keyType accepted {@link Key} type for this role * @param keyType accepted {@link Key} type for this role
* @param specType accepted {@link ContextSpec} type (may be a marker type) * @param specType accepted {@link ContextSpec} type (may be a marker type)
* @param factory constructor that builds a context for (key, spec) * @param factory constructor that builds a context for (key, spec)
* @param defaultSpec default spec supplier used when callers pass {@code null} * @param defaultSpec supplier of the default spec used when callers pass
* spec * {@code null}; capability metadata resolves one stable
* value during registration, while runtime creation retains
* the supplier contract
* @param <C> context type * @param <C> context type
* @param <K> key type * @param <K> key type
* @param <S> spec type * @param <S> spec type
* @throws NullPointerException if any class/factory/supplier argument is * @throws NullPointerException if any class, factory, supplier, or metadata
* {@code null} * default value is {@code null}
*/ */
protected <C extends CryptoContext, K extends Key, S extends ContextSpec> void capability(AlgorithmFamily family, protected <C extends CryptoContext, K extends Key, S extends ContextSpec> void capability(AlgorithmFamily family,
KeyUsage role, Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextConstructorKS<C, K, S> factory, KeyUsage role, Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextFactoryKS<C, K, S> factory,
Supplier<? extends S> defaultSpec) { Supplier<? extends S> defaultSpec) {
S resolvedDefault = Objects.requireNonNull(defaultSpec.get(), "defaultSpec value must not be null");
// bind runtime factory // bind runtime factory
bind(role, ctxType, keyType, specType, factory, defaultSpec); bindContext(role, ctxType, keyType, specType, factory, defaultSpec);
// publish metadata // publish metadata
addCapability(new Capability(id(), family, role, ctxType, keyType, specType, defaultSpec)); addCapability(new Capability(id(), family, role, ctxType, keyType, specType, resolvedDefault));
} }
} }

View File

@@ -34,7 +34,7 @@
package zeroecho.core.alg.aes; package zeroecho.core.alg.aes;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.SecureRandom; import java.util.Arrays;
import javax.crypto.KeyGenerator; import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
@@ -45,7 +45,9 @@ import zeroecho.core.KeyUsage;
import zeroecho.core.alg.AbstractCryptoAlgorithm; import zeroecho.core.alg.AbstractCryptoAlgorithm;
import zeroecho.core.context.EncryptionContext; import zeroecho.core.context.EncryptionContext;
import zeroecho.core.spec.VoidSpec; import zeroecho.core.spec.VoidSpec;
import zeroecho.core.spi.SymmetricKeyBuilder; import zeroecho.core.spi.SymmetricKeyGenerator;
import zeroecho.core.spi.SymmetricKeyImporter;
import zeroecho.sdk.util.RandomSupport;
/** /**
* AES algorithm registration and capability wiring. * AES algorithm registration and capability wiring.
@@ -85,49 +87,45 @@ public final class AesAlgorithm extends AbstractCryptoAlgorithm {
// Context capabilities // Context capabilities
capability(AlgorithmFamily.SYMMETRIC, KeyUsage.ENCRYPT, EncryptionContext.class, SecretKey.class, AesSpec.class, capability(AlgorithmFamily.SYMMETRIC, KeyUsage.ENCRYPT, EncryptionContext.class, SecretKey.class, AesSpec.class,
(SecretKey k, AesSpec s) -> new AesCipherContext(this, k, true, s, new SecureRandom()), (SecretKey k, AesSpec s) -> new AesCipherContext(this, k, true, s, RandomSupport.getRandom()),
() -> AesSpec.gcm128(null)); () -> AesSpec.gcm128(null));
capability(AlgorithmFamily.SYMMETRIC, KeyUsage.DECRYPT, EncryptionContext.class, SecretKey.class, AesSpec.class, capability(AlgorithmFamily.SYMMETRIC, KeyUsage.DECRYPT, EncryptionContext.class, SecretKey.class, AesSpec.class,
(SecretKey k, AesSpec s) -> new AesCipherContext(this, k, false, s, new SecureRandom()), (SecretKey k, AesSpec s) -> new AesCipherContext(this, k, false, s, RandomSupport.getRandom()),
() -> AesSpec.gcm128(null)); () -> AesSpec.gcm128(null));
capability(AlgorithmFamily.SYMMETRIC, KeyUsage.ENCRYPT, EncryptionContext.class, SecretKey.class, capability(AlgorithmFamily.SYMMETRIC, KeyUsage.ENCRYPT, EncryptionContext.class, SecretKey.class,
VoidSpec.class, (SecretKey k, VoidSpec s) -> new AesCipherContext(this, k, true, AesSpec.gcm128(null), VoidSpec.class, (SecretKey k, VoidSpec s) -> new AesCipherContext(this, k, true, AesSpec.gcm128(null),
new SecureRandom()), RandomSupport.getRandom()),
() -> VoidSpec.INSTANCE); () -> VoidSpec.INSTANCE);
capability(AlgorithmFamily.SYMMETRIC, KeyUsage.DECRYPT, EncryptionContext.class, SecretKey.class, capability(AlgorithmFamily.SYMMETRIC, KeyUsage.DECRYPT, EncryptionContext.class, SecretKey.class,
VoidSpec.class, (SecretKey k, VoidSpec s) -> new AesCipherContext(this, k, false, AesSpec.gcm128(null), VoidSpec.class, (SecretKey k, VoidSpec s) -> new AesCipherContext(this, k, false, AesSpec.gcm128(null),
new SecureRandom()), RandomSupport.getRandom()),
() -> VoidSpec.INSTANCE); () -> VoidSpec.INSTANCE);
// Secret generation builder (AesKeyGenSpec) // Secret generation builder (AesKeyGenSpec)
registerSymmetricKeyBuilder(AesKeyGenSpec.class, new SymmetricKeyBuilder<>() { registerSymmetricKeyGenerator(AesKeyGenSpec.class, new SymmetricKeyGenerator<>() {
@Override @Override
public SecretKey generateSecret(AesKeyGenSpec spec) throws GeneralSecurityException { public SecretKey generateSecret(AesKeyGenSpec spec) throws GeneralSecurityException {
KeyGenerator kg = KeyGenerator.getInstance("AES"); KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(spec.keySizeBits(), new SecureRandom()); kg.init(spec.keySizeBits(), RandomSupport.getRandom());
return kg.generateKey(); return kg.generateKey();
} }
@Override
public SecretKey importSecret(AesKeyGenSpec spec) {
throw new UnsupportedOperationException("Use AesKeyImportSpec for importing AES keys");
}
}, AesKeyGenSpec::aes256); }, AesKeyGenSpec::aes256);
// Secret import builder (AesKeyImportSpec) // Secret import builder (AesKeyImportSpec)
registerSymmetricKeyBuilder(AesKeyImportSpec.class, new SymmetricKeyBuilder<>() { registerSymmetricKeyImporter(AesKeyImportSpec.class, new SymmetricKeyImporter<>() {
@Override
public SecretKey generateSecret(AesKeyImportSpec spec) {
throw new UnsupportedOperationException("Use AesKeyGenSpec to generate AES keys");
}
@Override @Override
public SecretKey importSecret(AesKeyImportSpec spec) { public SecretKey importSecret(AesKeyImportSpec spec) {
return new SecretKeySpec(spec.key(), "AES"); byte[] key = spec.key();
try {
return new SecretKeySpec(key, "AES");
} finally {
Arrays.fill(key, (byte) 0);
}
} }
}, null); });
} }
} }

View File

@@ -55,6 +55,7 @@ import zeroecho.core.err.ProviderFailureException;
import zeroecho.core.io.CipherTransformInputStreamBuilder; import zeroecho.core.io.CipherTransformInputStreamBuilder;
import zeroecho.core.spi.ContextAware; import zeroecho.core.spi.ContextAware;
import zeroecho.core.util.Strings; import zeroecho.core.util.Strings;
import zeroecho.sdk.util.RandomSupport;
/** /**
* Streaming AES cipher context for GCM / CBC / CTR. * Streaming AES cipher context for GCM / CBC / CTR.
@@ -97,7 +98,8 @@ public final class AesCipherContext implements EncryptionContext, ContextAware {
* ({@code false}) * ({@code false})
* @param spec static AES settings (mode/padding and GCM tag bits); not * @param spec static AES settings (mode/padding and GCM tag bits); not
* null * null
* @param rnd secure random source; if null, a default is created * @param rnd secure random source; if null, the library's shared source
* is used
* @throws NullPointerException if any required parameter is null * @throws NullPointerException if any required parameter is null
* @throws IllegalArgumentException if {@code spec} is inconsistent (e.g., GCM * @throws IllegalArgumentException if {@code spec} is inconsistent (e.g., GCM
* without NOPADDING) * without NOPADDING)
@@ -107,7 +109,7 @@ public final class AesCipherContext implements EncryptionContext, ContextAware {
this.key = Objects.requireNonNull(key, "secret key must not be null"); this.key = Objects.requireNonNull(key, "secret key must not be null");
this.encrypt = encrypt; this.encrypt = encrypt;
this.spec = Objects.requireNonNull(spec, "spec must not be null"); this.spec = Objects.requireNonNull(spec, "spec must not be null");
this.rnd = (rnd != null ? rnd : new SecureRandom()); this.rnd = (rnd != null ? rnd : RandomSupport.getRandom());
} }
/** /**

View File

@@ -38,6 +38,9 @@ import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.Objects; 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;
import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.AlgorithmKeySpec;
@@ -48,8 +51,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <p> * <p>
* This class wraps raw key material (16, 24, or 32 bytes) for use with the AES * 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, * algorithm. Factory methods support construction from raw bytes, hex strings,
* or Base64-encoded strings. The key material is defensively copied to maintain * or Base64-encoded strings. The key material is defensively copied.
* immutability.
* </p> * </p>
* *
* <p> * <p>
@@ -58,13 +60,16 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* </p> * </p>
* *
* <p> * <p>
* Objects of this type are immutable and thread-safe. * Objects of this type are thread-safe while active and may be destroyed to wipe
* their owned key bytes. Access and marshalling fail after destruction.
* </p> * </p>
* *
* @since 1.0 * @since 1.0
*/ */
public final class AesKeyImportSpec implements AlgorithmKeySpec { public final class AesKeyImportSpec implements AlgorithmKeySpec, Destroyable {
private final byte[] key; private final byte[] key;
private final ReentrantLock lifecycleLock = new ReentrantLock();
private boolean destroyed;
private AesKeyImportSpec(byte[] key) { private AesKeyImportSpec(byte[] key) {
Objects.requireNonNull(key, "key must not be null"); Objects.requireNonNull(key, "key must not be null");
@@ -96,7 +101,12 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec {
*/ */
public static AesKeyImportSpec fromHex(String hex) { public static AesKeyImportSpec fromHex(String hex) {
Objects.requireNonNull(hex, "hex must not be null"); 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) { public static AesKeyImportSpec fromBase64(String b64) {
Objects.requireNonNull(b64, "base64 must not be null"); 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 * @return the raw key material
*/ */
public byte[] key() { 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 * @return a sequence containing the key data
*/ */
public static PairSeq marshal(AesKeyImportSpec spec) { 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); 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) { public static AesKeyImportSpec unmarshal(PairSeq p) {
byte[] out = null; byte[] out = null;
PairSeq.Cursor cur = p.cursor(); try {
while (cur.next()) { PairSeq.Cursor cur = p.cursor();
String k = cur.key(); while (cur.next()) {
String v = cur.value(); String k = cur.key();
switch (k) { String v = cur.value();
case "k.b64" -> out = Base64.getDecoder().decode(v); switch (k) {
case "k.hex" -> out = HexFormat.of().parseHex(v); case "k.b64" -> {
case "k.raw" -> out = v.getBytes(StandardCharsets.ISO_8859_1); wipe(out);
default -> { out = Base64.getDecoder().decode(v);
/* ignore */ } }
case "k.hex" -> {
wipe(out);
out = HexFormat.of().parseHex(v);
}
case "k.raw" -> {
wipe(out);
out = v.getBytes(StandardCharsets.ISO_8859_1);
}
default -> {
/* ignore */ }
}
} }
if (out == null) {
throw new IllegalArgumentException("AES key missing (k.b64 / k.hex / k.raw)");
}
return new AesKeyImportSpec(out);
} finally {
wipe(out);
} }
if (out == null) { }
throw new IllegalArgumentException("AES key missing (k.b64 / k.hex / k.raw)");
private static void wipe(byte[] current) {
if (current != null) {
Arrays.fill(current, (byte) 0);
}
}
private String encodedKey() {
lifecycleLock.lock();
try {
ensureActive();
return Base64.getEncoder().withoutPadding().encodeToString(key);
} finally {
lifecycleLock.unlock();
}
}
/** {@inheritDoc} */
@Override
public void destroy() {
lifecycleLock.lock();
try {
if (!destroyed) {
Arrays.fill(key, (byte) 0);
destroyed = true;
}
} finally {
lifecycleLock.unlock();
}
}
/** {@inheritDoc} */
@Override
public boolean isDestroyed() {
lifecycleLock.lock();
try {
return destroyed;
} finally {
lifecycleLock.unlock();
}
}
private void ensureActive() {
if (destroyed) {
throw new IllegalStateException("AES key import specification has been destroyed");
} }
return new AesKeyImportSpec(out);
} }
} }

View File

@@ -44,6 +44,7 @@ import java.security.PublicKey;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; 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.KemContext;
import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.context.MessageAgreementContext;
import zeroecho.core.spec.VoidSpec; import zeroecho.core.spec.VoidSpec;
import zeroecho.core.spi.AsymmetricKeyBuilder; import zeroecho.core.spi.AsymmetricKeyPairGenerator;
import zeroecho.core.spi.PrivateKeyImporter;
import zeroecho.core.spi.PublicKeyImporter;
/** /**
* <h2>Integration of BIKE (Bit Flipping Key Encapsulation) algorithm</h2> * <h2>Integration of BIKE (Bit Flipping Key Encapsulation) algorithm</h2>
@@ -84,14 +87,14 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* BikeAlgorithm bike = new BikeAlgorithm(); * BikeAlgorithm bike = new BikeAlgorithm();
* *
* // Generate a key pair * // Generate a key pair
* KeyPair kp = bike.asymmetricKeyBuilder(BikeKeyGenSpec.class) * KeyPair kp = bike.asymmetricKeyPairGenerator(BikeKeyGenSpec.class)
* .generateKeyPair(BikeKeyGenSpec.bike256()); * .generateKeyPair(BikeKeyGenSpec.bike256());
* *
* // Encapsulation using recipient's public key * // Encapsulation using recipient's public key
* KemContext kemEnc = bike.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE); * KemContext kemEnc = bike.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
* *
* // Decapsulation using private key * // Decapsulation using private key
* KemContext kemDec = bike.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE); * KemContext kemDec = bike.createContext(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
* }</pre> * }</pre>
* *
* @since 1.0 * @since 1.0
@@ -139,7 +142,7 @@ public final class BikeAlgorithm extends AbstractCryptoAlgorithm {
.build(); .build();
}, () -> VoidSpec.INSTANCE); }, () -> VoidSpec.INSTANCE);
registerAsymmetricKeyBuilder(BikeKeyGenSpec.class, new AsymmetricKeyBuilder<>() { registerAsymmetricKeyPairGenerator(BikeKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
@Override @Override
public KeyPair generateKeyPair(BikeKeyGenSpec spec) throws GeneralSecurityException { public KeyPair generateKeyPair(BikeKeyGenSpec spec) throws GeneralSecurityException {
ensureProvider(); ensureProvider();
@@ -152,23 +155,9 @@ public final class BikeAlgorithm extends AbstractCryptoAlgorithm {
kpg.initialize(params, new SecureRandom()); kpg.initialize(params, new SecureRandom());
return kpg.generateKeyPair(); return kpg.generateKeyPair();
} }
@Override
public PublicKey importPublic(BikeKeyGenSpec spec) {
throw new UnsupportedOperationException();
}
@Override
public PrivateKey importPrivate(BikeKeyGenSpec spec) {
throw new UnsupportedOperationException();
}
}, BikeKeyGenSpec::bike256); }, BikeKeyGenSpec::bike256);
registerAsymmetricKeyBuilder(BikePublicKeySpec.class, new AsymmetricKeyBuilder<>() { registerPublicKeyImporter(BikePublicKeySpec.class, new PublicKeyImporter<>() {
@Override
public KeyPair generateKeyPair(BikePublicKeySpec spec) {
throw new UnsupportedOperationException();
}
@Override @Override
public PublicKey importPublic(BikePublicKeySpec spec) throws GeneralSecurityException { public PublicKey importPublic(BikePublicKeySpec spec) throws GeneralSecurityException {
@@ -176,31 +165,22 @@ public final class BikeAlgorithm extends AbstractCryptoAlgorithm {
KeyFactory kf = KeyFactory.getInstance("BIKE", providerName()); KeyFactory kf = KeyFactory.getInstance("BIKE", providerName());
return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
} }
});
@Override registerPrivateKeyImporter(BikePrivateKeySpec.class, new PrivateKeyImporter<>() {
public PrivateKey importPrivate(BikePublicKeySpec spec) {
throw new UnsupportedOperationException();
}
}, null);
registerAsymmetricKeyBuilder(BikePrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
@Override
public KeyPair generateKeyPair(BikePrivateKeySpec spec) {
throw new UnsupportedOperationException();
}
@Override
public PublicKey importPublic(BikePrivateKeySpec spec) {
throw new UnsupportedOperationException();
}
@Override @Override
public PrivateKey importPrivate(BikePrivateKeySpec spec) throws GeneralSecurityException { public PrivateKey importPrivate(BikePrivateKeySpec spec) throws GeneralSecurityException {
ensureProvider(); ensureProvider();
KeyFactory kf = KeyFactory.getInstance("BIKE", providerName()); KeyFactory kf = KeyFactory.getInstance("BIKE", providerName());
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); byte[] encoded = spec.pkcs8();
try {
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
} finally {
Arrays.fill(encoded, (byte) 0);
}
} }
}, null); });
} }
/** /**

View File

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

View File

@@ -33,8 +33,12 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.core.alg.bike; package zeroecho.core.alg.bike;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Objects; 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;
import zeroecho.core.marshal.PairSeq.Cursor; import zeroecho.core.marshal.PairSeq.Cursor;
@@ -49,7 +53,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <h3>Usage</h3> <pre>{@code * <h3>Usage</h3> <pre>{@code
* // Import a BIKE private key * // Import a BIKE private key
* BikePrivateKeySpec spec = new BikePrivateKeySpec(pkcs8Bytes); * BikePrivateKeySpec spec = new BikePrivateKeySpec(pkcs8Bytes);
* PrivateKey key = bikeAlgorithm.importPrivate(spec); * PrivateKey key = bikeAlgorithm.privateKeyImporter(BikePrivateKeySpec.class).importPrivate(spec);
* *
* // Marshal for storage or transport * // Marshal for storage or transport
* PairSeq seq = BikePrivateKeySpec.marshal(spec); * PairSeq seq = BikePrivateKeySpec.marshal(spec);
@@ -60,10 +64,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* @since 1.0 * @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 static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] pkcs8; private final byte[] pkcs8;
private final ReentrantLock lifecycleLock = new ReentrantLock();
private boolean destroyed;
/** /**
* Constructs a new spec from a PKCS#8 encoded private key. * 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 * @return cloned PKCS#8 bytes
*/ */
public byte[] pkcs8() { 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 * @return serialized key representation
*/ */
public static PairSeq marshal(BikePrivateKeySpec spec) { 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); return PairSeq.of("type", "BikePrivateKeySpec", PKCS8_B64, b64);
} }
@@ -120,7 +132,12 @@ public final class BikePrivateKeySpec implements AlgorithmKeySpec {
if (b64 == null) { if (b64 == null) {
throw new IllegalArgumentException("BikePrivateKeySpec: missing pkcs8.b64"); 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() { public String toString() {
return "BikePrivateKeySpec[len=" + pkcs8.length + "]"; return "BikePrivateKeySpec[len=" + pkcs8.length + "]";
} }
private String encodedKey() {
lifecycleLock.lock();
try {
ensureActive();
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
} finally {
lifecycleLock.unlock();
}
}
@Override
public void destroy() {
lifecycleLock.lock();
try {
if (!destroyed) {
Arrays.fill(pkcs8, (byte) 0);
destroyed = true;
}
} finally {
lifecycleLock.unlock();
}
}
@Override
public boolean isDestroyed() {
lifecycleLock.lock();
try {
return destroyed;
} finally {
lifecycleLock.unlock();
}
}
private void ensureActive() {
if (destroyed) {
throw new IllegalStateException("BIKE private key specification has been destroyed");
}
}
} }

View File

@@ -49,7 +49,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <h3>Usage</h3> <pre>{@code * <h3>Usage</h3> <pre>{@code
* // Import a BIKE public key * // Import a BIKE public key
* BikePublicKeySpec spec = new BikePublicKeySpec(x509Bytes); * BikePublicKeySpec spec = new BikePublicKeySpec(x509Bytes);
* PublicKey key = bikeAlgorithm.importPublic(spec); * PublicKey key = bikeAlgorithm.publicKeyImporter(BikePublicKeySpec.class).importPublic(spec);
* *
* // Marshal for transport or storage * // Marshal for transport or storage
* PairSeq seq = BikePublicKeySpec.marshal(spec); * PairSeq seq = BikePublicKeySpec.marshal(spec);

View File

@@ -35,13 +35,15 @@ package zeroecho.core.alg.chacha;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.KeyGenerator; import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import zeroecho.core.alg.AbstractCryptoAlgorithm; import zeroecho.core.alg.AbstractCryptoAlgorithm;
import zeroecho.core.spi.SymmetricKeyBuilder; import zeroecho.core.spi.SymmetricKeyGenerator;
import zeroecho.core.spi.SymmetricKeyImporter;
/** /**
* <h2>Abstract base for ChaCha family algorithms</h2> * <h2>Abstract base for ChaCha family algorithms</h2>
@@ -64,19 +66,20 @@ import zeroecho.core.spi.SymmetricKeyBuilder;
* {@code "ChaCha20"}.</li> * {@code "ChaCha20"}.</li>
* <li>Import wraps the raw key material with * <li>Import wraps the raw key material with
* {@link javax.crypto.spec.SecretKeySpec}.</li> * {@link javax.crypto.spec.SecretKeySpec}.</li>
* <li>Attempts to generate a key via {@code ChaChaKeyImportSpec} or import via * <li>Generation and import are discovered through independent exact
* {@code ChaChaKeyGenSpec} will throw * capabilities, so an unsupported lookup fails before invocation.</li>
* {@link UnsupportedOperationException}.</li>
* </ul> * </ul>
* *
* <h3>Example</h3> <pre>{@code * <h3>Example</h3> <pre>{@code
* AbstractChaChaAlgorithm algo = ...; * ZeroEchoSession session = new ZeroEchoSession();
* *
* // Generate a fresh 256-bit key * // 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 * // Import an existing key
* SecretKey imported = algo.importSecret(new ChaChaKeyImportSpec(rawBytes)); * SecretKey imported = session.keyBuilders().symmetric()
* .importKey("ChaCha20", new ChaChaKeyImportSpec(rawBytes));
* }</pre> * }</pre>
* *
* @since 1.0 * @since 1.0
@@ -93,30 +96,26 @@ abstract class AbstractChaChaAlgorithm extends AbstractCryptoAlgorithm {
super(id, title); super(id, title);
// register once for both algorithms (same 256-bit key) // register once for both algorithms (same 256-bit key)
registerSymmetricKeyBuilder(ChaChaKeyGenSpec.class, new SymmetricKeyBuilder<>() { registerSymmetricKeyGenerator(ChaChaKeyGenSpec.class, new SymmetricKeyGenerator<>() {
@Override @Override
public SecretKey generateSecret(ChaChaKeyGenSpec spec) throws GeneralSecurityException { public SecretKey generateSecret(ChaChaKeyGenSpec spec) throws GeneralSecurityException {
KeyGenerator kg = KeyGenerator.getInstance("ChaCha20"); KeyGenerator kg = KeyGenerator.getInstance("ChaCha20");
kg.init(spec.keySizeBits(), new SecureRandom()); kg.init(spec.keySizeBits(), new SecureRandom());
return kg.generateKey(); return kg.generateKey();
} }
@Override
public SecretKey importSecret(ChaChaKeyGenSpec spec) {
throw new UnsupportedOperationException("Use ChaChaKeyImportSpec for importing ChaCha keys");
}
}, ChaChaKeyGenSpec::chacha256); }, ChaChaKeyGenSpec::chacha256);
registerSymmetricKeyBuilder(ChaChaKeyImportSpec.class, new SymmetricKeyBuilder<>() { registerSymmetricKeyImporter(ChaChaKeyImportSpec.class, new SymmetricKeyImporter<>() {
@Override
public SecretKey generateSecret(ChaChaKeyImportSpec spec) {
throw new UnsupportedOperationException("Use ChaChaKeyGenSpec to generate ChaCha keys");
}
@Override @Override
public SecretKey importSecret(ChaChaKeyImportSpec spec) { public SecretKey importSecret(ChaChaKeyImportSpec spec) {
return new SecretKeySpec(spec.key(), "ChaCha20"); byte[] key = spec.key();
try {
return new SecretKeySpec(key, "ChaCha20");
} finally {
Arrays.fill(key, (byte) 0);
}
} }
}, null); });
} }
} }

View File

@@ -74,19 +74,19 @@ import zeroecho.core.SymmetricHeaderCodec;
* corresponding cipher context. * corresponding cipher context.
* *
* <h3>Example</h3> <pre>{@code * <h3>Example</h3> <pre>{@code
* var algo = new ChaCha20Poly1305Algorithm(); * ZeroEchoSession session = new ZeroEchoSession();
* SecretKey key = algo.generateSecret(ChaChaKeyGenSpec.chacha256()); * SecretKey key = session.keyBuilders().symmetric()
* .generate("CHACHA20-POLY1305", ChaChaKeyGenSpec.chacha256());
* *
* // Encrypt with explicit spec * // Encrypt with explicit spec
* var spec = ChaCha20Poly1305Spec.builder().header(null).build(); * ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(null).build();
* EncryptionContext enc = algo.newContext( * EncryptionContext enc = session.createContext(
* zeroecho.core.AlgorithmFamily.SYMMETRIC, * "CHACHA20-POLY1305", zeroecho.core.KeyUsage.ENCRYPT, key, spec);
* zeroecho.core.KeyUsage.ENCRYPT, key, spec);
* *
* // Decrypt using VoidSpec default * // Decrypt using VoidSpec default
* EncryptionContext dec = algo.newContext( * EncryptionContext dec = session.createContext(
* zeroecho.core.AlgorithmFamily.SYMMETRIC, * "CHACHA20-POLY1305", zeroecho.core.KeyUsage.DECRYPT, key,
* zeroecho.core.KeyUsage.DECRYPT, key, zeroecho.core.spec.VoidSpec.INSTANCE); * zeroecho.core.spec.VoidSpec.INSTANCE);
* }</pre> * }</pre>
* *
* @since 1.0 * @since 1.0

View File

@@ -38,6 +38,9 @@ import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.Objects; 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;
import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.AlgorithmKeySpec;
@@ -66,7 +69,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <h3>Usage</h3> <pre>{@code * <h3>Usage</h3> <pre>{@code
* // Import from raw key bytes * // Import from raw key bytes
* ChaChaKeyImportSpec spec = ChaChaKeyImportSpec.fromRaw(keyBytes); * ChaChaKeyImportSpec spec = ChaChaKeyImportSpec.fromRaw(keyBytes);
* SecretKey key = cryptoAlgorithm.importSecret(spec); * SecretKey key = cryptoAlgorithm.symmetricKeyImporter(ChaChaKeyImportSpec.class).importSecret(spec);
* *
* // Serialize to PairSeq * // Serialize to PairSeq
* PairSeq seq = ChaChaKeyImportSpec.marshal(spec); * PairSeq seq = ChaChaKeyImportSpec.marshal(spec);
@@ -77,8 +80,10 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* @since 1.0 * @since 1.0
*/ */
public final class ChaChaKeyImportSpec implements AlgorithmKeySpec { public final class ChaChaKeyImportSpec implements AlgorithmKeySpec, Destroyable {
private final byte[] key; private final byte[] key;
private final ReentrantLock lifecycleLock = new ReentrantLock();
private boolean destroyed;
/** /**
* Creates a new import spec with the given raw key. * 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 * @return spec wrapping the decoded key
*/ */
public static ChaChaKeyImportSpec fromHex(String hex) { 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 * @return spec wrapping the decoded key
*/ */
public static ChaChaKeyImportSpec fromBase64(String b64) { 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 * @return 32-byte key array
*/ */
public byte[] key() { 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 * @return serialized key representation
*/ */
public static PairSeq marshal(ChaChaKeyImportSpec spec) { 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); 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) { public static ChaChaKeyImportSpec unmarshal(PairSeq p) {
byte[] out = null; byte[] out = null;
PairSeq.Cursor c = p.cursor(); try {
while (c.next()) { PairSeq.Cursor c = p.cursor();
String k = c.key(); while (c.next()) {
String v = c.value(); String k = c.key();
switch (k) { String v = c.value();
case "k.b64" -> out = Base64.getDecoder().decode(v); switch (k) {
case "k.hex" -> out = HexFormat.of().parseHex(v); case "k.b64" -> {
case "k.raw" -> out = v.getBytes(StandardCharsets.ISO_8859_1); wipe(out);
default -> { out = Base64.getDecoder().decode(v);
}
case "k.hex" -> {
wipe(out);
out = HexFormat.of().parseHex(v);
}
case "k.raw" -> {
wipe(out);
out = v.getBytes(StandardCharsets.ISO_8859_1);
}
default -> {
}
} }
} }
if (out == null) {
throw new IllegalArgumentException("ChaCha20 key missing (k.b64 / k.hex / k.raw)");
}
return new ChaChaKeyImportSpec(out);
} finally {
wipe(out);
} }
if (out == null) { }
throw new IllegalArgumentException("ChaCha20 key missing (k.b64 / k.hex / k.raw)");
private static void wipe(byte[] current) {
if (current != null) {
Arrays.fill(current, (byte) 0);
}
}
private String encodedKey() {
lifecycleLock.lock();
try {
ensureActive();
return Base64.getEncoder().withoutPadding().encodeToString(key);
} finally {
lifecycleLock.unlock();
}
}
/** {@inheritDoc} */
@Override
public void destroy() {
lifecycleLock.lock();
try {
if (!destroyed) {
Arrays.fill(key, (byte) 0);
destroyed = true;
}
} finally {
lifecycleLock.unlock();
}
}
/** {@inheritDoc} */
@Override
public boolean isDestroyed() {
lifecycleLock.lock();
try {
return destroyed;
} finally {
lifecycleLock.unlock();
}
}
private void ensureActive() {
if (destroyed) {
throw new IllegalStateException("ChaCha key import specification has been destroyed");
} }
return new ChaChaKeyImportSpec(out);
} }
} }

View File

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

View File

@@ -44,6 +44,7 @@ import java.security.PublicKey;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; 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.KemContext;
import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.context.MessageAgreementContext;
import zeroecho.core.spec.VoidSpec; import zeroecho.core.spec.VoidSpec;
import zeroecho.core.spi.AsymmetricKeyBuilder; import zeroecho.core.spi.AsymmetricKeyPairGenerator;
import zeroecho.core.spi.PrivateKeyImporter;
import zeroecho.core.spi.PublicKeyImporter;
/** /**
* <h2>Classic McEliece (CMCE) algorithm adapter</h2> * <h2>Classic McEliece (CMCE) algorithm adapter</h2>
@@ -106,15 +109,15 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* CmceAlgorithm alg = new CmceAlgorithm(); * CmceAlgorithm alg = new CmceAlgorithm();
* *
* // Generate a key pair with a chosen CMCE variant. * // Generate a key pair with a chosen CMCE variant.
* KeyPair kp = alg.asymmetricKeyBuilder(CmceKeyGenSpec.class) * KeyPair kp = alg.asymmetricKeyPairGenerator(CmceKeyGenSpec.class)
* .generateKeyPair(CmceKeyGenSpec.mceliece8192128f()); * .generateKeyPair(CmceKeyGenSpec.mceliece8192128f());
* *
* // Create a KEM encapsulation context with the recipient public key. * // 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. * // Create an agreement initiator context backed by CMCE KEM.
* MessageAgreementContext initiator = * MessageAgreementContext initiator =
* alg.create(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE); * alg.createContext(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE);
* }</pre> * }</pre>
* *
* @since 1.0 * @since 1.0
@@ -169,7 +172,7 @@ public final class CmceAlgorithm extends AbstractCryptoAlgorithm {
.build(); .build();
}, () -> VoidSpec.INSTANCE); }, () -> VoidSpec.INSTANCE);
registerAsymmetricKeyBuilder(CmceKeyGenSpec.class, new AsymmetricKeyBuilder<>() { registerAsymmetricKeyPairGenerator(CmceKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
@Override @Override
public KeyPair generateKeyPair(CmceKeyGenSpec spec) throws GeneralSecurityException { public KeyPair generateKeyPair(CmceKeyGenSpec spec) throws GeneralSecurityException {
ensureProvider(); ensureProvider();
@@ -189,23 +192,9 @@ public final class CmceAlgorithm extends AbstractCryptoAlgorithm {
kpg.initialize(params, new SecureRandom()); kpg.initialize(params, new SecureRandom());
return kpg.generateKeyPair(); return kpg.generateKeyPair();
} }
@Override
public PublicKey importPublic(CmceKeyGenSpec spec) {
throw new UnsupportedOperationException();
}
@Override
public PrivateKey importPrivate(CmceKeyGenSpec spec) {
throw new UnsupportedOperationException();
}
}, CmceKeyGenSpec::mceliece8192128f); }, CmceKeyGenSpec::mceliece8192128f);
registerAsymmetricKeyBuilder(CmcePublicKeySpec.class, new AsymmetricKeyBuilder<>() { registerPublicKeyImporter(CmcePublicKeySpec.class, new PublicKeyImporter<>() {
@Override
public KeyPair generateKeyPair(CmcePublicKeySpec spec) {
throw new UnsupportedOperationException();
}
@Override @Override
public PublicKey importPublic(CmcePublicKeySpec spec) throws GeneralSecurityException { public PublicKey importPublic(CmcePublicKeySpec spec) throws GeneralSecurityException {
@@ -213,31 +202,22 @@ public final class CmceAlgorithm extends AbstractCryptoAlgorithm {
KeyFactory kf = KeyFactory.getInstance("CMCE", providerName()); KeyFactory kf = KeyFactory.getInstance("CMCE", providerName());
return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
} }
});
@Override registerPrivateKeyImporter(CmcePrivateKeySpec.class, new PrivateKeyImporter<>() {
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();
}
@Override @Override
public PrivateKey importPrivate(CmcePrivateKeySpec spec) throws GeneralSecurityException { public PrivateKey importPrivate(CmcePrivateKeySpec spec) throws GeneralSecurityException {
ensureProvider(); ensureProvider();
KeyFactory kf = KeyFactory.getInstance("CMCE", providerName()); 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 { private static void ensureProvider() throws NoSuchProviderException {

View File

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

View File

@@ -33,8 +33,12 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.core.alg.cmce; package zeroecho.core.alg.cmce;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Objects; 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;
import zeroecho.core.marshal.PairSeq.Cursor; import zeroecho.core.marshal.PairSeq.Cursor;
@@ -49,8 +53,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* </p> * </p>
* *
* <p> * <p>
* Instances are immutable. The internal byte array is cloned on construction * The internal byte array is cloned on construction and on every accessor.
* and on every accessor to prevent accidental mutation. * Access and destruction are synchronized.
* </p> * </p>
* *
* <h2>Marshalling</h2> * <h2>Marshalling</h2>
@@ -76,10 +80,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* @since 1.0 * @since 1.0
*/ */
public final class CmcePrivateKeySpec implements AlgorithmKeySpec { public final class CmcePrivateKeySpec implements AlgorithmKeySpec, Destroyable {
private static final String PKCS8_B64 = "pkcs8.b64"; private static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] pkcs8; private final byte[] pkcs8;
private final ReentrantLock lifecycleLock = new ReentrantLock();
private boolean destroyed;
/** /**
* Creates a new specification from a PKCS#8-encoded CMCE private key. * Creates a new specification from a PKCS#8-encoded CMCE private key.
@@ -101,7 +107,13 @@ public final class CmcePrivateKeySpec implements AlgorithmKeySpec {
* @return a fresh copy of the underlying PKCS#8 encoding * @return a fresh copy of the underlying PKCS#8 encoding
*/ */
public byte[] pkcs8() { public byte[] pkcs8() {
return pkcs8.clone(); lifecycleLock.lock();
try {
ensureActive();
return pkcs8.clone();
} finally {
lifecycleLock.unlock();
}
} }
/** /**
@@ -118,7 +130,7 @@ public final class CmcePrivateKeySpec implements AlgorithmKeySpec {
* @throws NullPointerException if {@code spec} is null * @throws NullPointerException if {@code spec} is null
*/ */
public static PairSeq marshal(CmcePrivateKeySpec spec) { public static PairSeq marshal(CmcePrivateKeySpec spec) {
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); String b64 = spec.encodedKey();
return PairSeq.of("type", "CmcePrivateKeySpec", PKCS8_B64, b64); return PairSeq.of("type", "CmcePrivateKeySpec", PKCS8_B64, b64);
} }
@@ -144,7 +156,12 @@ public final class CmcePrivateKeySpec implements AlgorithmKeySpec {
if (b64 == null) { if (b64 == null) {
throw new IllegalArgumentException("CmcePrivateKeySpec: missing pkcs8.b64"); throw new IllegalArgumentException("CmcePrivateKeySpec: missing pkcs8.b64");
} }
return new CmcePrivateKeySpec(Base64.getDecoder().decode(b64)); byte[] decoded = Base64.getDecoder().decode(b64);
try {
return new CmcePrivateKeySpec(decoded);
} finally {
Arrays.fill(decoded, (byte) 0);
}
} }
/** /**
@@ -160,4 +177,43 @@ public final class CmcePrivateKeySpec implements AlgorithmKeySpec {
public String toString() { public String toString() {
return "CmcePrivateKeySpec[len=" + pkcs8.length + "]"; return "CmcePrivateKeySpec[len=" + pkcs8.length + "]";
} }
private String encodedKey() {
lifecycleLock.lock();
try {
ensureActive();
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
} finally {
lifecycleLock.unlock();
}
}
@Override
public void destroy() {
lifecycleLock.lock();
try {
if (!destroyed) {
Arrays.fill(pkcs8, (byte) 0);
destroyed = true;
}
} finally {
lifecycleLock.unlock();
}
}
@Override
public boolean isDestroyed() {
lifecycleLock.lock();
try {
return destroyed;
} finally {
lifecycleLock.unlock();
}
}
private void ensureActive() {
if (destroyed) {
throw new IllegalStateException("CMCE private key specification has been destroyed");
}
}
} }

View File

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

View File

@@ -33,21 +33,18 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.core.alg.common.agreement; package zeroecho.core.alg.common.agreement;
import java.security.GeneralSecurityException;
import java.security.Key; import java.security.Key;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.PublicKey; import java.security.PublicKey;
import javax.crypto.KeyAgreement;
import zeroecho.core.CryptoAlgorithm; import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.context.AgreementContext; import zeroecho.core.context.AgreementContext;
/** /**
* <h2>Generic JCA-based Key Agreement Context</h2> * <h2>Generic JCA-based Key Agreement Context</h2>
* *
* An {@link AgreementContext} backed by the standard JCA {@link KeyAgreement} * An {@link AgreementContext} backed by the standard JCA key-agreement API.
* API. This class supports elliptic-curve and modern Diffie-Hellman variants * This class supports elliptic-curve and modern Diffie-Hellman variants
* such as ECDH, XDH (X25519, X448), and others provided by the runtime or * such as ECDH, XDH (X25519, X448), and others provided by the runtime or
* configured provider. * configured provider.
* *
@@ -75,12 +72,9 @@ import zeroecho.core.context.AgreementContext;
* *
* @since 1.0 * @since 1.0
*/ */
public class GenericJcaAgreementContext implements AgreementContext { public final class GenericJcaAgreementContext implements AgreementContext {
private final CryptoAlgorithm algorithm; private final CryptoAlgorithm algorithm;
private final PrivateKey privateKey; private final JcaAgreementEngine engine;
private final String jcaName; // e.g., "ECDH" or "XDH" (or "X25519"/"X448")
private final String provider; // null => default
private PublicKey peer;
/** /**
* Creates a new JCA-based agreement context. * Creates a new JCA-based agreement context.
@@ -95,10 +89,8 @@ public class GenericJcaAgreementContext implements AgreementContext {
* is {@code null} * is {@code null}
*/ */
public GenericJcaAgreementContext(CryptoAlgorithm alg, PrivateKey priv, String jcaName, String provider) { public GenericJcaAgreementContext(CryptoAlgorithm alg, PrivateKey priv, String jcaName, String provider) {
this.algorithm = alg; this.algorithm = java.util.Objects.requireNonNull(alg, "alg must not be null");
this.privateKey = priv; this.engine = new JcaAgreementEngine(priv, jcaName, provider);
this.jcaName = jcaName;
this.provider = provider;
} }
/** /**
@@ -118,7 +110,7 @@ public class GenericJcaAgreementContext implements AgreementContext {
*/ */
@Override @Override
public Key key() { public Key key() {
return privateKey; return engine.privateKey();
} }
/** /**
@@ -133,7 +125,7 @@ public class GenericJcaAgreementContext implements AgreementContext {
*/ */
@Override @Override
public void setPeerPublic(PublicKey peer) { public void setPeerPublic(PublicKey peer) {
this.peer = peer; engine.setPeerPublic(peer);
} }
/** /**
@@ -141,7 +133,7 @@ public class GenericJcaAgreementContext implements AgreementContext {
* previously assigned peer public key. * previously assigned peer public key.
* *
* <p> * <p>
* Internally this delegates to the JCA {@link KeyAgreement} API with the given * Internally this delegates to the JCA key-agreement API with the given
* {@code jcaName} and optional provider. * {@code jcaName} and optional provider.
* </p> * </p>
* *
@@ -152,18 +144,7 @@ public class GenericJcaAgreementContext implements AgreementContext {
*/ */
@Override @Override
public byte[] deriveSecret() { public byte[] deriveSecret() {
if (peer == null) { return engine.deriveSecret();
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);
}
} }
/** /**

View File

@@ -104,9 +104,9 @@ import zeroecho.core.context.MessageAgreementContext;
* *
* @since 1.0 * @since 1.0
*/ */
public final class GenericJcaMessageAgreementContext extends GenericJcaAgreementContext public final class GenericJcaMessageAgreementContext implements MessageAgreementContext {
implements MessageAgreementContext {
private final GenericJcaAgreementContext agreement;
private final PublicKey localPublic; private final PublicKey localPublic;
private final String keyFactoryAlg; private final String keyFactoryAlg;
private final String keyFactoryProvider; private final String keyFactoryProvider;
@@ -139,7 +139,8 @@ public final class GenericJcaMessageAgreementContext extends GenericJcaAgreement
*/ */
public GenericJcaMessageAgreementContext(CryptoAlgorithm alg, KeyPairKey keyPairKey, String jcaAgreementName, public GenericJcaMessageAgreementContext(CryptoAlgorithm alg, KeyPairKey keyPairKey, String jcaAgreementName,
String agreementProvider, String keyFactoryAlg, String keyFactoryProvider) { 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); Objects.requireNonNull(jcaAgreementName, "jcaAgreementName"), agreementProvider);
this.localPublic = Objects.requireNonNull(keyPairKey.publicKey(), "keyPairKey.public"); this.localPublic = Objects.requireNonNull(keyPairKey.publicKey(), "keyPairKey.public");
this.keyFactoryAlg = Objects.requireNonNull(keyFactoryAlg, "keyFactoryAlg"); this.keyFactoryAlg = Objects.requireNonNull(keyFactoryAlg, "keyFactoryAlg");
@@ -199,12 +200,18 @@ public final class GenericJcaMessageAgreementContext extends GenericJcaAgreement
@Override @Override
public void setPeerMessage(byte[] message) { public void setPeerMessage(byte[] message) {
if (message == null) { if (message == null) {
setPeerPublic(null); agreement.setPeerPublic(null);
return; return;
} }
PublicKey peerPublic = importPeerPublic(message); 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); throw new IllegalArgumentException("Failed to import peer public key using KeyFactory " + keyFactoryAlg, e);
} }
} }
/** {@inheritDoc} */
@Override
public byte[] deriveSecret() {
return agreement.deriveSecret();
}
/** {@inheritDoc} */
@Override
public CryptoAlgorithm algorithm() {
return agreement.algorithm();
}
/** {@inheritDoc} */
@Override
public java.security.Key key() {
return agreement.key();
}
/** {@inheritDoc} */
@Override
public void close() {
agreement.close();
}
} }

View File

@@ -0,0 +1,54 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the conditions in the project LICENSE are met.
******************************************************************************/
package zeroecho.core.alg.common.agreement;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Objects;
import javax.crypto.KeyAgreement;
/**
* Package-private reusable JCA agreement mechanics.
*/
final class JcaAgreementEngine {
private final PrivateKey privateKey;
private final String jcaName;
private final String provider;
private PublicKey peer;
/* default */ JcaAgreementEngine(PrivateKey privateKey, String jcaName, String provider) {
this.privateKey = Objects.requireNonNull(privateKey, "privateKey must not be null");
this.jcaName = Objects.requireNonNull(jcaName, "jcaName must not be null");
this.provider = provider;
}
/* default */ PrivateKey privateKey() {
return privateKey;
}
/* default */ void setPeerPublic(PublicKey peer) {
this.peer = peer;
}
/* default */ byte[] deriveSecret() {
if (peer == null) {
throw new IllegalStateException("Peer public key not set");
}
try {
KeyAgreement agreement = provider == null ? KeyAgreement.getInstance(jcaName)
: KeyAgreement.getInstance(jcaName, provider);
agreement.init(privateKey);
agreement.doPhase(peer, true);
return agreement.generateSecret();
} catch (GeneralSecurityException exception) {
throw new IllegalArgumentException("KeyAgreement failed for " + jcaName, exception);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -81,8 +81,8 @@ import zeroecho.core.util.Strings;
* upstream stream. * upstream stream.
* </p> * </p>
*/ */
final class Stream extends AbstractPassthroughInputStream { final class SignatureStream extends AbstractPassthroughInputStream {
private static final Logger LOG = Logger.getLogger(Stream.class.getName()); private static final Logger LOG = Logger.getLogger(SignatureStream.class.getName());
/** Cached trailer in sign mode: computed once from {@link Signature#sign()}. */ /** Cached trailer in sign mode: computed once from {@link Signature#sign()}. */
private byte[] signature; private byte[] signature;
@@ -108,7 +108,7 @@ final class Stream extends AbstractPassthroughInputStream {
* @param strategy verification predicate used in verify mode; ignored in * @param strategy verification predicate used in verify mode; ignored in
* sign mode; must not be {@code null} in verify mode * sign mode; must not be {@code null} in verify mode
*/ */
/* package */ Stream(final Signature engine, final boolean signMode, final InputStream upstream, /* package */ SignatureStream(final Signature engine, final boolean signMode, final InputStream upstream,
final int bodyBufSize, final byte[] expectedTag, final VerificationBiPredicate<Signature> strategy) { final int bodyBufSize, final byte[] expectedTag, final VerificationBiPredicate<Signature> strategy) {
super(upstream, bodyBufSize); super(upstream, bodyBufSize);
this.engine = engine; this.engine = engine;

View File

@@ -60,7 +60,7 @@
* configured {@link java.security.Signature}, resolves a fixed tag length (via * configured {@link java.security.Signature}, resolves a fixed tag length (via
* resolvers), and exposes a one-shot {@code wrap(InputStream)} API. * resolvers), and exposes a one-shot {@code wrap(InputStream)} API.
* Verification behavior is controlled by a pluggable comparison approach.</li> * Verification behavior is controlled by a pluggable comparison approach.</li>
* <li><b>Stream</b> - internal passthrough input stream that feeds chunks to * <li><b>SignatureStream</b> - internal passthrough input stream that feeds chunks to
* the signature engine, emits the trailer in SIGN mode, and performs final * the signature engine, emits the trailer in SIGN mode, and performs final
* verification in VERIFY mode.</li> * verification in VERIFY mode.</li>
* </ul> * </ul>

View File

@@ -35,11 +35,11 @@ package zeroecho.core.alg.dh;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.KeyFactory; import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import zeroecho.core.AlgorithmFamily; import zeroecho.core.AlgorithmFamily;
import zeroecho.core.KeyUsage; 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.alg.common.agreement.KeyPairKey;
import zeroecho.core.context.AgreementContext; import zeroecho.core.context.AgreementContext;
import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.context.MessageAgreementContext;
import zeroecho.core.spi.AsymmetricKeyBuilder; import zeroecho.core.spi.PrivateKeyImporter;
import zeroecho.core.spi.PublicKeyImporter;
/** /**
* Diffie-Hellman algorithm registration for use in the pluggable cryptography * Diffie-Hellman algorithm registration for use in the pluggable cryptography
@@ -88,10 +89,10 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* DhSpec spec = DhSpec.ffdhe3072(); * DhSpec spec = DhSpec.ffdhe3072();
* *
* // Generate a key pair using the registered builder * // Generate a key pair using the registered builder
* KeyPair kp = CryptoAlgorithms.keyPair("DH", spec); * KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("DH", spec);
* *
* // Obtain an agreement context for DH key agreement * // Obtain an agreement context for DH key agreement
* AgreementContext ctx = CryptoAlgorithms.create("DH", KeyUsage.AGREEMENT, kp.getPrivate(), spec); * AgreementContext ctx = session.createContext("DH", KeyUsage.AGREEMENT, kp.getPrivate(), spec);
* *
* // Use the context with a peer public key to derive a shared secret * // Use the context with a peer public key to derive a shared secret
* ctx.setPeerPublic(peerPublicKey); * ctx.setPeerPublic(peerPublicKey);
@@ -140,43 +141,28 @@ public final class DhAlgorithm extends AbstractCryptoAlgorithm {
"DiffieHellman", null, "DH", null), "DiffieHellman", null, "DH", null),
DhSpec::ffdhe2048); DhSpec::ffdhe2048);
registerAsymmetricKeyBuilder(DhSpec.class, new DhKeyGenBuilder(), DhSpec::ffdhe2048); registerAsymmetricKeyPairGenerator(DhSpec.class, new DhKeyGenBuilder(), DhSpec::ffdhe2048);
registerAsymmetricKeyBuilder(DhPublicKeySpec.class, new AsymmetricKeyBuilder<>() { registerPublicKeyImporter(DhPublicKeySpec.class, new PublicKeyImporter<>() {
@Override
public KeyPair generateKeyPair(DhPublicKeySpec spec) throws GeneralSecurityException {
throw new UnsupportedOperationException("Use DhKeyGenBuilder for keypair generation.");
}
@Override @Override
public PublicKey importPublic(DhPublicKeySpec spec) throws GeneralSecurityException { public PublicKey importPublic(DhPublicKeySpec spec) throws GeneralSecurityException {
KeyFactory kf = KeyFactory.getInstance("DH"); KeyFactory kf = KeyFactory.getInstance("DH");
return kf.generatePublic(new X509EncodedKeySpec(spec.encoded())); return kf.generatePublic(new X509EncodedKeySpec(spec.encoded()));
} }
});
@Override registerPrivateKeyImporter(DhPrivateKeySpec.class, new PrivateKeyImporter<>() {
public PrivateKey importPrivate(DhPublicKeySpec spec) throws GeneralSecurityException {
throw new UnsupportedOperationException("Use DhPrivateKeySpec for private key import.");
}
}, null);
registerAsymmetricKeyBuilder(DhPrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
@Override
public KeyPair generateKeyPair(DhPrivateKeySpec spec) throws GeneralSecurityException {
throw new UnsupportedOperationException("Use DhKeyGenBuilder for keypair generation.");
}
@Override
public PublicKey importPublic(DhPrivateKeySpec spec) throws GeneralSecurityException {
throw new UnsupportedOperationException("Use DhPrivateKeySpec for public key import.");
}
@Override @Override
public PrivateKey importPrivate(DhPrivateKeySpec spec) throws GeneralSecurityException { public PrivateKey importPrivate(DhPrivateKeySpec spec) throws GeneralSecurityException {
KeyFactory kf = KeyFactory.getInstance("DH"); KeyFactory kf = KeyFactory.getInstance("DH");
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); byte[] encoded = spec.encoded();
try {
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
} finally {
Arrays.fill(encoded, (byte) 0);
}
} }
}, null); });
} }
} }

View File

@@ -38,12 +38,10 @@ import java.security.AlgorithmParameters;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.KeyPairGenerator; import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import javax.crypto.spec.DHParameterSpec; import javax.crypto.spec.DHParameterSpec;
import zeroecho.core.spi.AsymmetricKeyBuilder; import zeroecho.core.spi.AsymmetricKeyPairGenerator;
/** /**
* <h2>DH key pair builder</h2> * <h2>DH key pair builder</h2>
@@ -89,7 +87,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* KeyPair kp2 = builder.generateKeyPair(sized); * KeyPair kp2 = builder.generateKeyPair(sized);
* }</pre> * }</pre>
*/ */
public final class DhKeyGenBuilder implements AsymmetricKeyBuilder<DhSpec> { public final class DhKeyGenBuilder implements AsymmetricKeyPairGenerator<DhSpec> {
/** /**
* Generates a Diffie-Hellman key pair for the given specification. * Generates a Diffie-Hellman key pair for the given specification.
* *
@@ -138,56 +136,4 @@ public final class DhKeyGenBuilder implements AsymmetricKeyBuilder<DhSpec> {
kpg.initialize(dh); kpg.initialize(dh);
return kpg.generateKeyPair(); return kpg.generateKeyPair();
} }
/**
* Unsupported for DH in this builder.
*
* <p>
* Raw public key import is not implemented because this builder focuses on key
* generation from DH parameters. Use higher-level catalog or codec facilities
* to parse or construct {@link PublicKey} instances if needed.
* </p>
*
* <p>
* <strong>Example</strong>
* </p>
* <pre>{@code
* // This will throw UnsupportedOperationException
* new DhKeyGenBuilder().importPublic(DhSpec.ffdhe2048());
* }</pre>
*
* @param spec the DH specification (ignored)
* @return never returns normally
* @throws UnsupportedOperationException always thrown
*/
@Override
public PublicKey importPublic(DhSpec spec) {
throw new UnsupportedOperationException();
}
/**
* Unsupported for DH in this builder.
*
* <p>
* Raw private key import is not implemented because this builder focuses on key
* generation from DH parameters. Use higher-level catalog or codec facilities
* to parse or construct {@link PrivateKey} instances if needed.
* </p>
*
* <p>
* <strong>Example</strong>
* </p>
* <pre>{@code
* // This will throw UnsupportedOperationException
* new DhKeyGenBuilder().importPrivate(DhSpec.ffdhe2048());
* }</pre>
*
* @param spec the DH specification (ignored)
* @return never returns normally
* @throws UnsupportedOperationException always thrown
*/
@Override
public PrivateKey importPrivate(DhSpec spec) {
throw new UnsupportedOperationException();
}
} }

View File

@@ -33,7 +33,11 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.core.alg.dh; package zeroecho.core.alg.dh;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.concurrent.locks.ReentrantLock;
import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq;
import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.AlgorithmKeySpec;
@@ -50,8 +54,9 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* <h2>Design</h2> * <h2>Design</h2>
* <ul> * <ul>
* <li>Immutable: the internal byte array is defensively copied at construction * <li>Destroyable: the internal byte array is defensively copied at
* and when returned by {@link #encoded()}.</li> * construction and when returned by {@link #encoded()}, and is cleared by
* {@link #destroy()}.</li>
* <li>Encodable: supports marshaling to/from a * <li>Encodable: supports marshaling to/from a
* {@link zeroecho.core.marshal.PairSeq} so keys can be serialized in * {@link zeroecho.core.marshal.PairSeq} so keys can be serialized in
* human-readable or protocol-friendly formats.</li> * human-readable or protocol-friendly formats.</li>
@@ -62,7 +67,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <h2>Example</h2> <pre>{@code * <h2>Example</h2> <pre>{@code
* // Import a DH private key from encoded bytes * // Import a DH private key from encoded bytes
* DhPrivateKeySpec spec = new DhPrivateKeySpec(pkcs8Bytes); * 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 * // Marshal to a text-friendly representation
* PairSeq ps = DhPrivateKeySpec.marshal(spec); * PairSeq ps = DhPrivateKeySpec.marshal(spec);
@@ -73,10 +78,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* @since 1.0 * @since 1.0
*/ */
public class DhPrivateKeySpec implements AlgorithmKeySpec { public class DhPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
private static final String PKCS8_B64 = "pkcs8.b64"; private static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] pkcs8; 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. * 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 * @return a defensive copy of the PKCS#8 encoded DH private key
*/ */
public byte[] encoded() { 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} * @throws NullPointerException if {@code spec} is {@code null}
*/ */
public static PairSeq marshal(DhPrivateKeySpec spec) { 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); return PairSeq.of("type", "DH-PRIV", PKCS8_B64, b64);
} }
@@ -137,12 +150,62 @@ public class DhPrivateKeySpec implements AlgorithmKeySpec {
String k = cur.key(); String k = cur.key();
String v = cur.value(); String v = cur.value();
if (PKCS8_B64.equals(k)) { if (PKCS8_B64.equals(k)) {
out = Base64.getDecoder().decode(v); out = decodeReplacing(out, v);
} }
} }
if (out == null) { if (out == null) {
throw new IllegalArgumentException("pkcs8.b64 missing for DH private key"); throw new IllegalArgumentException("pkcs8.b64 missing for DH private key");
} }
return new DhPrivateKeySpec(out); try {
return new DhPrivateKeySpec(out);
} finally {
Arrays.fill(out, (byte) 0);
}
}
private static byte[] decodeReplacing(byte[] current, String encoded) {
if (current != null) {
Arrays.fill(current, (byte) 0);
}
return Base64.getDecoder().decode(encoded);
}
private String encodedKey() {
lifecycleLock.lock();
try {
ensureActive();
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
} finally {
lifecycleLock.unlock();
}
}
@Override
public void destroy() {
lifecycleLock.lock();
try {
if (!destroyed) {
Arrays.fill(pkcs8, (byte) 0);
destroyed = true;
}
} finally {
lifecycleLock.unlock();
}
}
@Override
public boolean isDestroyed() {
lifecycleLock.lock();
try {
return destroyed;
} finally {
lifecycleLock.unlock();
}
}
private void ensureActive() {
if (destroyed) {
throw new IllegalStateException("DH private key specification has been destroyed");
}
} }
} }

View File

@@ -64,7 +64,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <h2>Example</h2> <pre>{@code * <h2>Example</h2> <pre>{@code
* // Import a DH public key from encoded bytes * // Import a DH public key from encoded bytes
* DhPublicKeySpec spec = new DhPublicKeySpec(x509Bytes); * 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 * // Marshal to a text-friendly representation
* PairSeq ps = DhPublicKeySpec.marshal(spec); * PairSeq ps = DhPublicKeySpec.marshal(spec);

View File

@@ -80,10 +80,10 @@ import zeroecho.core.spec.ContextSpec;
* *
* <h2>Example</h2> <pre>{@code * <h2>Example</h2> <pre>{@code
* // Create a key pair in the FFDHE-3072 group * // 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 * // Establish an agreement context
* AgreementContext ctx = CryptoAlgorithms.create( * AgreementContext ctx = session.createContext(
* "DH", KeyUsage.AGREEMENT, kp.getPrivate(), DhSpec.ffdhe3072()); * "DH", KeyUsage.AGREEMENT, kp.getPrivate(), DhSpec.ffdhe3072());
* }</pre> * }</pre>
* *

View File

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

View File

@@ -76,7 +76,7 @@ import zeroecho.core.spec.ContextSpec;
* DigestSpec spec = DigestSpec.shake256(64); * DigestSpec spec = DigestSpec.shake256(64);
* *
* // Use in context creation * // Use in context creation
* DigestContext ctx = CryptoAlgorithms.create( * DigestContext ctx = session.createContext(
* "DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, spec); * "DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, spec);
* *
* byte[] digest = ctx.doFinal(data); * byte[] digest = ctx.doFinal(data);

View File

@@ -33,7 +33,6 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.core.alg.digest; package zeroecho.core.alg.digest;
import java.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.MessageDigest; import java.security.MessageDigest;
@@ -43,6 +42,7 @@ import zeroecho.core.KeyUsage;
import zeroecho.core.NullKey; import zeroecho.core.NullKey;
import zeroecho.core.alg.AbstractCryptoAlgorithm; import zeroecho.core.alg.AbstractCryptoAlgorithm;
import zeroecho.core.context.DigestContext; import zeroecho.core.context.DigestContext;
import zeroecho.core.err.ProviderFailureException;
/** /**
* <h2>SHA-2, SHA-3, and SHAKE digest algorithms</h2> * <h2>SHA-2, SHA-3, and SHAKE digest algorithms</h2>
@@ -77,7 +77,7 @@ import zeroecho.core.context.DigestContext;
* CryptoAlgorithm algo = CryptoAlgorithms.require("DIGEST"); * CryptoAlgorithm algo = CryptoAlgorithms.require("DIGEST");
* *
* // Create a digest context for SHA3-512 * // Create a digest context for SHA3-512
* DigestContext ctx = CryptoAlgorithms.create( * DigestContext ctx = session.createContext(
* "DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, DigestSpec.sha3_512()); * "DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, DigestSpec.sha3_512());
* *
* // Stream data into the digest * // Stream data into the digest
@@ -117,7 +117,8 @@ public final class Sha2Sha3Algorithm extends AbstractCryptoAlgorithm {
MessageDigest md = MessageDigest.getInstance(s.algorithm().jca()); MessageDigest md = MessageDigest.getInstance(s.algorithm().jca());
return new JcaDigestContext(this, md, s); return new JcaDigestContext(this, md, s);
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
throw new IOException("Failed to init MessageDigest: " + s.algorithm().jca(), e); throw new ProviderFailureException(
"Failed to initialize MessageDigest " + s.algorithm().jca(), e);
} }
}, DigestSpec::sha256 // default for catalog/tests }, DigestSpec::sha256 // default for catalog/tests
); );

View File

@@ -90,18 +90,18 @@ import zeroecho.core.context.MessageAgreementContext;
* *
* <h2>Example</h2> <pre>{@code * <h2>Example</h2> <pre>{@code
* // Generate a key pair for Alice * // 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 * // 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 * // Alice computes shared secret using her private key
* AgreementContext aliceCtx = CryptoAlgorithms.create("ECDH", * AgreementContext aliceCtx = session.createContext("ECDH",
* KeyUsage.AGREEMENT, aliceKeys.getPrivate(), EcdsaCurveSpec.P256); * KeyUsage.AGREEMENT, aliceKeys.getPrivate(), EcdsaCurveSpec.P256);
* byte[] aliceSecret = aliceCtx.derive(bobKeys.getPublic()); * byte[] aliceSecret = aliceCtx.derive(bobKeys.getPublic());
* *
* // Bob computes shared secret using his private key * // Bob computes shared secret using his private key
* AgreementContext bobCtx = CryptoAlgorithms.create("ECDH", * AgreementContext bobCtx = session.createContext("ECDH",
* KeyUsage.AGREEMENT, bobKeys.getPrivate(), EcdsaCurveSpec.P256); * KeyUsage.AGREEMENT, bobKeys.getPrivate(), EcdsaCurveSpec.P256);
* byte[] bobSecret = bobCtx.derive(aliceKeys.getPublic()); * byte[] bobSecret = bobCtx.derive(aliceKeys.getPublic());
* *
@@ -160,8 +160,9 @@ public final class EcdhAlgorithm extends AbstractCryptoAlgorithm {
() -> EcdsaCurveSpec.P256); () -> EcdsaCurveSpec.P256);
// Reuse EC builders/importers // Reuse EC builders/importers
registerAsymmetricKeyBuilder(EcdhCurveSpec.class, new EcdhKeyGenBuilder(), () -> EcdhCurveSpec.P256); registerAsymmetricKeyPairGenerator(EcdhCurveSpec.class, new EcdhKeyGenBuilder(),
registerAsymmetricKeyBuilder(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder(), null); () -> EcdhCurveSpec.P256);
registerAsymmetricKeyBuilder(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder(), null); registerPublicKeyImporter(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder());
registerPrivateKeyImporter(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder());
} }
} }

View File

@@ -63,7 +63,7 @@ import zeroecho.core.spec.ContextSpec;
* *
* <h2>Usage</h2> <pre>{@code * <h2>Usage</h2> <pre>{@code
* // Generate a key pair on P-256 * // 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 * // Use curve metadata
* String jcaName = EcdhCurveSpec.P256.curveName(); // "secp256r1" * String jcaName = EcdhCurveSpec.P256.curveName(); // "secp256r1"

View File

@@ -39,15 +39,13 @@ import java.security.KeyPairGenerator;
import java.security.spec.ECGenParameterSpec; import java.security.spec.ECGenParameterSpec;
import zeroecho.core.alg.ecdsa.EcdsaPrivateKeyBuilder; import zeroecho.core.alg.ecdsa.EcdsaPrivateKeyBuilder;
import zeroecho.core.alg.ecdsa.EcdsaPrivateKeySpec;
import zeroecho.core.alg.ecdsa.EcdsaPublicKeyBuilder; import zeroecho.core.alg.ecdsa.EcdsaPublicKeyBuilder;
import zeroecho.core.alg.ecdsa.EcdsaPublicKeySpec; import zeroecho.core.spi.AsymmetricKeyPairGenerator;
import zeroecho.core.spi.AsymmetricKeyBuilder;
/** /**
* <h2>ECDH Key Pair Generator</h2> * <h2>ECDH Key Pair Generator</h2>
* *
* Implementation of {@link AsymmetricKeyBuilder} for elliptic curve * Implementation of {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} for elliptic curve
* Diffie-Hellman (ECDH) key pairs. * Diffie-Hellman (ECDH) key pairs.
* *
* <p> * <p>
@@ -70,7 +68,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* *
* @since 1.0 * @since 1.0
*/ */
public final class EcdhKeyGenBuilder implements AsymmetricKeyBuilder<EcdhCurveSpec> { public final class EcdhKeyGenBuilder implements AsymmetricKeyPairGenerator<EcdhCurveSpec> {
/** /**
* Generates a new elliptic curve key pair for use in ECDH key agreement. * Generates a new elliptic curve key pair for use in ECDH key agreement.
* *
@@ -92,40 +90,4 @@ public final class EcdhKeyGenBuilder implements AsymmetricKeyBuilder<EcdhCurveSp
kpg.initialize(new ECGenParameterSpec(spec.curveName())); kpg.initialize(new ECGenParameterSpec(spec.curveName()));
return kpg.generateKeyPair(); return kpg.generateKeyPair();
} }
/**
* Unsupported operation for this builder.
*
* <p>
* Importing existing ECDH public keys should be performed via
* {@link EcdsaPublicKeyBuilder} with an {@link EcdsaPublicKeySpec}. This method
* will always throw an {@link UnsupportedOperationException}.
* </p>
*
* @param spec ignored
* @return never returns normally
* @throws UnsupportedOperationException always
*/
@Override
public java.security.PublicKey importPublic(EcdhCurveSpec spec) {
throw new UnsupportedOperationException("Use EcdhPublicKeySpec with EcdsaPublicKeyBuilder.");
}
/**
* Unsupported operation for this builder.
*
* <p>
* Importing existing ECDH private keys should be performed via
* {@link EcdsaPrivateKeyBuilder} with an {@link EcdsaPrivateKeySpec}. This
* method will always throw an {@link UnsupportedOperationException}.
* </p>
*
* @param spec ignored
* @return never returns normally
* @throws UnsupportedOperationException always
*/
@Override
public java.security.PrivateKey importPrivate(EcdhCurveSpec spec) {
throw new UnsupportedOperationException("Use EcdhPrivateKeySpec with EcdsaPrivateKeyBuilder.");
}
} }

View File

@@ -39,7 +39,6 @@ import java.security.PublicKey;
import zeroecho.core.AlgorithmFamily; import zeroecho.core.AlgorithmFamily;
import zeroecho.core.CryptoAlgorithm; import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.CryptoCatalog; import zeroecho.core.CryptoCatalog;
import zeroecho.core.KeyUsage; import zeroecho.core.KeyUsage;
import zeroecho.core.alg.AbstractCryptoAlgorithm; import zeroecho.core.alg.AbstractCryptoAlgorithm;
@@ -82,9 +81,9 @@ import zeroecho.core.context.SignatureContext;
* *
* <pre>{@code * <pre>{@code
* // Example: Sign and verify with ECDSA/P-256 * // Example: Sign and verify with ECDSA/P-256
* KeyPair kp = CryptoAlgorithms.keyPair("ECDSA", EcdsaCurveSpec.P256); * KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("ECDSA", EcdsaCurveSpec.P256);
* SignatureContext signer = CryptoAlgorithms.create("ECDSA", KeyUsage.SIGN, kp.getPrivate(), EcdsaCurveSpec.P256); * SignatureContext signer = session.createContext("ECDSA", KeyUsage.SIGN, kp.getPrivate(), EcdsaCurveSpec.P256);
* SignatureContext verifier = CryptoAlgorithms.create("ECDSA", KeyUsage.VERIFY, kp.getPublic(), EcdsaCurveSpec.P256); * SignatureContext verifier = session.createContext("ECDSA", KeyUsage.VERIFY, kp.getPublic(), EcdsaCurveSpec.P256);
* }</pre> * }</pre>
* *
* @since 1.0 * @since 1.0
@@ -104,8 +103,8 @@ public final class EcdsaAlgorithm extends AbstractCryptoAlgorithm {
* <p> * <p>
* On construction, the algorithm declares its supported roles and registers * On construction, the algorithm declares its supported roles and registers
* builders with the {@link CryptoAlgorithm} infrastructure so they can be * builders with the {@link CryptoAlgorithm} infrastructure so they can be
* discovered by the {@link CryptoCatalog} or invoked through * discovered by the {@link CryptoCatalog} or invoked through the
* {@link CryptoAlgorithms} convenience methods. * session-bound {@link zeroecho.sdk.KeyBuilders} entry point.
* </p> * </p>
*/ */
public EcdsaAlgorithm() { public EcdsaAlgorithm() {
@@ -135,8 +134,9 @@ public final class EcdsaAlgorithm extends AbstractCryptoAlgorithm {
} }
}, () -> EcdsaCurveSpec.P256); }, () -> EcdsaCurveSpec.P256);
registerAsymmetricKeyBuilder(EcdsaCurveSpec.class, new EcdsaKeyGenBuilder(), () -> EcdsaCurveSpec.P256); registerAsymmetricKeyPairGenerator(EcdsaCurveSpec.class, new EcdsaKeyGenBuilder(),
registerAsymmetricKeyBuilder(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder(), null); () -> EcdsaCurveSpec.P256);
registerAsymmetricKeyBuilder(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder(), null); registerPublicKeyImporter(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder());
registerPrivateKeyImporter(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder());
} }
} }

View File

@@ -39,30 +39,23 @@ import java.security.KeyPairGenerator;
import java.security.spec.ECGenParameterSpec; import java.security.spec.ECGenParameterSpec;
import zeroecho.core.CryptoAlgorithm; import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.CryptoAlgorithms; import zeroecho.core.spi.AsymmetricKeyPairGenerator;
import zeroecho.core.spi.AsymmetricKeyBuilder;
/** /**
* <h2>ECDSA Key Pair Generator</h2> * <h2>ECDSA Key Pair Generator</h2>
* *
* Implementation of {@link AsymmetricKeyBuilder} for {@link EcdsaCurveSpec}. * Implementation of {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} for
* {@link EcdsaCurveSpec}.
* This builder is responsible for generating new elliptic curve key pairs for * This builder is responsible for generating new elliptic curve key pairs for
* use with the {@link EcdsaAlgorithm}. * use with the {@link EcdsaAlgorithm}.
* *
* <h2>Supported operations</h2> * <p>The exact supported operation is
* <ul> * {@link #generateKeyPair(EcdsaCurveSpec)}. Public and private import are
* <li>{@link #generateKeyPair(EcdsaCurveSpec)} - create a fresh key pair for * registered separately through {@link EcdsaPublicKeyBuilder} and
* the given named curve.</li> * {@link EcdsaPrivateKeyBuilder}.</p>
* <li>{@link #importPublic(EcdsaCurveSpec)} - unsupported; use
* {@link EcdsaPublicKeySpec} with {@link EcdsaPublicKeyBuilder} instead.</li>
* <li>{@link #importPrivate(EcdsaCurveSpec)} - unsupported; use
* {@link EcdsaPrivateKeySpec} with {@link EcdsaPrivateKeyBuilder} instead.</li>
* </ul>
* *
* <h2>Usage</h2> Typically accessed indirectly through * <h2>Usage</h2> Typically accessed through the session key-operation API or
* {@link CryptoAlgorithms#keyPair(String, zeroecho.core.spec.AlgorithmKeySpec)} * {@link CryptoAlgorithm#asymmetricKeyPairGenerator(Class)}.
* or
* {@link CryptoAlgorithm#generateKeyPair(zeroecho.core.spec.AlgorithmKeySpec)}.
* *
* <pre>{@code * <pre>{@code
* // Example: Generate an ECDSA P-256 key pair * // Example: Generate an ECDSA P-256 key pair
@@ -72,7 +65,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* *
* @since 1.0 * @since 1.0
*/ */
public final class EcdsaKeyGenBuilder implements AsymmetricKeyBuilder<EcdsaCurveSpec> { public final class EcdsaKeyGenBuilder implements AsymmetricKeyPairGenerator<EcdsaCurveSpec> {
/** /**
* Generates a new elliptic curve key pair for the given curve specification. * Generates a new elliptic curve key pair for the given curve specification.
* *
@@ -93,38 +86,4 @@ public final class EcdsaKeyGenBuilder implements AsymmetricKeyBuilder<EcdsaCurve
kpg.initialize(new ECGenParameterSpec(spec.curveName())); kpg.initialize(new ECGenParameterSpec(spec.curveName()));
return kpg.generateKeyPair(); return kpg.generateKeyPair();
} }
/**
* Unsupported operation for this builder.
*
* <p>
* Public key import should be performed using {@link EcdsaPublicKeySpec} and
* {@link EcdsaPublicKeyBuilder}.
* </p>
*
* @param spec unused curve specification
* @return never returns normally
* @throws UnsupportedOperationException always thrown
*/
@Override
public java.security.PublicKey importPublic(EcdsaCurveSpec spec) {
throw new UnsupportedOperationException("Use EcdsaPublicKeySpec with EcdsaPublicKeyBuilder.");
}
/**
* Unsupported operation for this builder.
*
* <p>
* Private key import should be performed using {@link EcdsaPrivateKeySpec} and
* {@link EcdsaPrivateKeyBuilder}.
* </p>
*
* @param spec unused curve specification
* @return never returns normally
* @throws UnsupportedOperationException always thrown
*/
@Override
public java.security.PrivateKey importPrivate(EcdsaCurveSpec spec) {
throw new UnsupportedOperationException("Use EcdsaPrivateKeySpec with EcdsaPrivateKeyBuilder.");
}
} }

View File

@@ -37,36 +37,28 @@ import java.security.GeneralSecurityException;
import java.security.KeyFactory; import java.security.KeyFactory;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
import zeroecho.core.CryptoAlgorithm; import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.CryptoAlgorithms; import zeroecho.core.spi.PrivateKeyImporter;
import zeroecho.core.spi.AsymmetricKeyBuilder;
/** /**
* <h2>ECDSA Private Key Builder</h2> * <h2>ECDSA Private Key Builder</h2>
* *
* Implementation of {@link AsymmetricKeyBuilder} for * Implementation of {@link zeroecho.core.spi.PrivateKeyImporter} for
* {@link EcdsaPrivateKeySpec}. This builder is responsible for importing ECDSA * {@link EcdsaPrivateKeySpec}. This builder is responsible for importing ECDSA
* private keys from encoded representations. * private keys from encoded representations.
* *
* <h2>Supported operations</h2> * <p>The exact supported operation is
* <ul> * {@link #importPrivate(EcdsaPrivateKeySpec)}. Generation and public import are
* <li>{@link #importPrivate(EcdsaPrivateKeySpec)} - construct a * registered through their own operation-specific implementations.</p>
* {@link PrivateKey} instance from a PKCS#8 encoded key.</li>
* <li>{@link #generateKeyPair(EcdsaPrivateKeySpec)} - unsupported; use
* {@link EcdsaKeyGenBuilder} instead.</li>
* <li>{@link #importPublic(EcdsaPrivateKeySpec)} - unsupported; use
* {@link EcdsaPublicKeySpec} with {@link EcdsaPublicKeyBuilder} instead.</li>
* </ul>
* *
* <h2>Encoding</h2> The {@link EcdsaPrivateKeySpec} stores the private key in * <h2>Encoding</h2> The {@link EcdsaPrivateKeySpec} stores the private key in
* PKCS#8 DER format. This builder delegates to a JCA {@link KeyFactory} for the * PKCS#8 DER format. This builder delegates to a JCA {@link KeyFactory} for the
* {@code "EC"} algorithm to reconstruct a usable {@link PrivateKey}. * {@code "EC"} algorithm to reconstruct a usable {@link PrivateKey}.
* *
* <h2>Usage</h2> Typically accessed indirectly through * <h2>Usage</h2> Typically accessed through the session key-operation API or
* {@link CryptoAlgorithms#privateKey(String, zeroecho.core.spec.AlgorithmKeySpec)} * {@link CryptoAlgorithm#privateKeyImporter(Class)}.
* or
* {@link CryptoAlgorithm#importPrivate(zeroecho.core.spec.AlgorithmKeySpec)}.
* *
* <pre>{@code * <pre>{@code
* // Example: Import an ECDSA private key * // Example: Import an ECDSA private key
@@ -77,40 +69,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* *
* @since 1.0 * @since 1.0
*/ */
public final class EcdsaPrivateKeyBuilder implements AsymmetricKeyBuilder<EcdsaPrivateKeySpec> { public final class EcdsaPrivateKeyBuilder implements PrivateKeyImporter<EcdsaPrivateKeySpec> {
/**
* Unsupported operation for this builder.
*
* <p>
* ECDSA key pair generation should be performed using
* {@link EcdsaKeyGenBuilder}, not from a private key specification.
* </p>
*
* @param spec unused private key specification
* @return never returns normally
* @throws UnsupportedOperationException always thrown
*/
@Override
public java.security.KeyPair generateKeyPair(EcdsaPrivateKeySpec spec) {
throw new UnsupportedOperationException("Use EcdsaKeyGenBuilder for keypair generation.");
}
/**
* Unsupported operation for this builder.
*
* <p>
* Public key import should be performed using {@link EcdsaPublicKeySpec} with
* {@link EcdsaPublicKeyBuilder}.
* </p>
*
* @param spec unused private key specification
* @return never returns normally
* @throws UnsupportedOperationException always thrown
*/
@Override
public java.security.PublicKey importPublic(EcdsaPrivateKeySpec spec) {
throw new UnsupportedOperationException("Use EcdsaPublicKeySpec with EcdsaPublicKeyBuilder.");
}
/** /**
* Imports a private key from a PKCS#8 encoded specification. * Imports a private key from a PKCS#8 encoded specification.
@@ -128,6 +87,11 @@ public final class EcdsaPrivateKeyBuilder implements AsymmetricKeyBuilder<EcdsaP
@Override @Override
public PrivateKey importPrivate(EcdsaPrivateKeySpec spec) throws GeneralSecurityException { public PrivateKey importPrivate(EcdsaPrivateKeySpec spec) throws GeneralSecurityException {
KeyFactory kf = KeyFactory.getInstance("EC"); KeyFactory kf = KeyFactory.getInstance("EC");
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); byte[] encoded = spec.encoded();
try {
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
} finally {
Arrays.fill(encoded, (byte) 0);
}
} }
} }

View File

@@ -33,7 +33,11 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.core.alg.ecdsa; package zeroecho.core.alg.ecdsa;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.concurrent.locks.ReentrantLock;
import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq;
import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.AlgorithmKeySpec;
@@ -41,7 +45,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
/** /**
* <h2>ECDSA Private Key Specification</h2> * <h2>ECDSA Private Key Specification</h2>
* *
* An immutable wrapper around a PKCS#8-encoded ECDSA private key. This * A destroyable wrapper around a PKCS#8-encoded ECDSA private key. This
* specification is used by {@link EcdsaPrivateKeyBuilder} to import keys into * specification is used by {@link EcdsaPrivateKeyBuilder} to import keys into
* the JCA {@link java.security.PrivateKey} representation. * the JCA {@link java.security.PrivateKey} representation.
* *
@@ -69,10 +73,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* @since 1.0 * @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 static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] pkcs8; 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. * 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 * @return cloned PKCS#8 byte array
*/ */
public byte[] encoded() { 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 * @return serialized representation in key-value form
*/ */
public static PairSeq marshal(EcdsaPrivateKeySpec spec) { 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); return PairSeq.of("type", "ECDSA-PRIV", PKCS8_B64, b64);
} }
@@ -131,12 +143,62 @@ public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec {
String k = cur.key(); String k = cur.key();
String v = cur.value(); String v = cur.value();
if (PKCS8_B64.equals(k)) { if (PKCS8_B64.equals(k)) {
out = Base64.getDecoder().decode(v); out = decodeReplacing(out, v);
} }
} }
if (out == null) { if (out == null) {
throw new IllegalArgumentException("pkcs8.b64 missing for ECDSA private key"); throw new IllegalArgumentException("pkcs8.b64 missing for ECDSA private key");
} }
return new EcdsaPrivateKeySpec(out); try {
return new EcdsaPrivateKeySpec(out);
} finally {
Arrays.fill(out, (byte) 0);
}
}
private static byte[] decodeReplacing(byte[] current, String encoded) {
if (current != null) {
Arrays.fill(current, (byte) 0);
}
return Base64.getDecoder().decode(encoded);
}
private String encodedKey() {
lifecycleLock.lock();
try {
ensureActive();
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
} finally {
lifecycleLock.unlock();
}
}
@Override
public void destroy() {
lifecycleLock.lock();
try {
if (!destroyed) {
Arrays.fill(pkcs8, (byte) 0);
destroyed = true;
}
} finally {
lifecycleLock.unlock();
}
}
@Override
public boolean isDestroyed() {
lifecycleLock.lock();
try {
return destroyed;
} finally {
lifecycleLock.unlock();
}
}
private void ensureActive() {
if (destroyed) {
throw new IllegalStateException("ECDSA private key specification has been destroyed");
}
} }
} }

View File

@@ -39,33 +39,25 @@ import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
import zeroecho.core.CryptoAlgorithm; import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.CryptoAlgorithms; import zeroecho.core.spi.PublicKeyImporter;
import zeroecho.core.spi.AsymmetricKeyBuilder;
/** /**
* <h2>ECDSA Public Key Builder</h2> * <h2>ECDSA Public Key Builder</h2>
* *
* Implementation of {@link AsymmetricKeyBuilder} for * Implementation of {@link zeroecho.core.spi.PublicKeyImporter} for
* {@link EcdsaPublicKeySpec}. This builder is responsible for importing ECDSA * {@link EcdsaPublicKeySpec}. This builder is responsible for importing ECDSA
* public keys from X.509 SubjectPublicKeyInfo encodings. * public keys from X.509 SubjectPublicKeyInfo encodings.
* *
* <h2>Supported operations</h2> * <p>The exact supported operation is
* <ul> * {@link #importPublic(EcdsaPublicKeySpec)}. Generation and private import are
* <li>{@link #importPublic(EcdsaPublicKeySpec)} - construct a {@link PublicKey} * registered through their own operation-specific implementations.</p>
* instance from an X.509-encoded key.</li>
* <li>{@link #generateKeyPair(EcdsaPublicKeySpec)} - unsupported; use
* {@link EcdsaKeyGenBuilder} instead.</li>
* <li>{@link #importPrivate(EcdsaPublicKeySpec)} - unsupported; use
* {@link EcdsaPrivateKeySpec} with {@link EcdsaPrivateKeyBuilder} instead.</li>
* </ul>
* *
* <h2>Encoding</h2> The {@link EcdsaPublicKeySpec} stores the public key in * <h2>Encoding</h2> The {@link EcdsaPublicKeySpec} stores the public key in
* standard X.509 DER format. This builder delegates to a JCA {@link KeyFactory} * standard X.509 DER format. This builder delegates to a JCA {@link KeyFactory}
* for the {@code "EC"} algorithm to reconstruct a usable {@link PublicKey}. * for the {@code "EC"} algorithm to reconstruct a usable {@link PublicKey}.
* *
* <h2>Usage</h2> Typically accessed indirectly through * <h2>Usage</h2> Typically accessed through the session key-operation API or
* {@link CryptoAlgorithms#publicKey(String, zeroecho.core.spec.AlgorithmKeySpec)} * {@link CryptoAlgorithm#publicKeyImporter(Class)}.
* or {@link CryptoAlgorithm#importPublic(zeroecho.core.spec.AlgorithmKeySpec)}.
* *
* <pre>{@code * <pre>{@code
* // Example: Import an ECDSA public key * // Example: Import an ECDSA public key
@@ -76,23 +68,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* *
* @since 1.0 * @since 1.0
*/ */
public final class EcdsaPublicKeyBuilder implements AsymmetricKeyBuilder<EcdsaPublicKeySpec> { public final class EcdsaPublicKeyBuilder implements PublicKeyImporter<EcdsaPublicKeySpec> {
/**
* Unsupported operation for this builder.
*
* <p>
* ECDSA key pair generation should be performed using
* {@link EcdsaKeyGenBuilder}, not from a public key specification.
* </p>
*
* @param spec unused public key specification
* @return never returns normally
* @throws UnsupportedOperationException always thrown
*/
@Override
public java.security.KeyPair generateKeyPair(EcdsaPublicKeySpec spec) {
throw new UnsupportedOperationException("Use EcdsaKeyGenBuilder for keypair generation.");
}
/** /**
* Imports a public key from an X.509 SubjectPublicKeyInfo specification. * Imports a public key from an X.509 SubjectPublicKeyInfo specification.
@@ -112,21 +88,4 @@ public final class EcdsaPublicKeyBuilder implements AsymmetricKeyBuilder<EcdsaPu
KeyFactory kf = KeyFactory.getInstance("EC"); KeyFactory kf = KeyFactory.getInstance("EC");
return kf.generatePublic(new X509EncodedKeySpec(spec.encoded())); return kf.generatePublic(new X509EncodedKeySpec(spec.encoded()));
} }
/**
* Unsupported operation for this builder.
*
* <p>
* Private key import should be performed using {@link EcdsaPrivateKeySpec} with
* {@link EcdsaPrivateKeyBuilder}.
* </p>
*
* @param spec unused public key specification
* @return never returns normally
* @throws UnsupportedOperationException always thrown
*/
@Override
public java.security.PrivateKey importPrivate(EcdsaPublicKeySpec spec) {
throw new UnsupportedOperationException("Use EcdsaPrivateKeySpec with EcdsaPrivateKeyBuilder.");
}
} }

View File

@@ -36,9 +36,9 @@
* *
* <p> * <p>
* This package provides the ECDSA algorithm descriptor, curve specifications, * This package provides the ECDSA algorithm descriptor, curve specifications,
* key builders for generation and import, and immutable encoded key specs. It * key builders for generation and import, and defensively copying encoded key
* wires ECDSA into the core signature SPI through a JCA-backed streaming * specs. It wires ECDSA into the core signature SPI through a JCA-backed
* signature context that enforces fixed signature lengths. * streaming signature context that enforces fixed signature lengths.
* </p> * </p>
* *
* <h2>Scope and responsibilities</h2> * <h2>Scope and responsibilities</h2>
@@ -66,8 +66,9 @@
* <li><b>EcdsaPublicKeyBuilder</b> and <b>EcdsaPrivateKeyBuilder:</b> import * <li><b>EcdsaPublicKeyBuilder</b> and <b>EcdsaPrivateKeyBuilder:</b> import
* keys from X.509 and PKCS#8 encodings via * keys from X.509 and PKCS#8 encodings via
* {@link java.security.KeyFactory}.</li> * {@link java.security.KeyFactory}.</li>
* <li><b>EcdsaPublicKeySpec</b> and <b>EcdsaPrivateKeySpec:</b> immutable * <li><b>EcdsaPublicKeySpec</b> and <b>EcdsaPrivateKeySpec:</b> wrappers around
* wrappers around encoded keys with marshalling support.</li> * encoded keys with marshalling support; the private-key form is
* destroyable.</li>
* </ul> * </ul>
* *
* <h2>Design notes</h2> * <h2>Design notes</h2>

View File

@@ -122,9 +122,9 @@ public final class Ed25519Algorithm extends AbstractCryptoAlgorithm {
}, () -> VoidSpec.INSTANCE); }, () -> VoidSpec.INSTANCE);
// Key builders // Key builders
registerAsymmetricKeyBuilder(Ed25519KeyGenSpec.class, new Ed25519KeyGenBuilder(), registerAsymmetricKeyPairGenerator(Ed25519KeyGenSpec.class, new Ed25519KeyGenBuilder(),
Ed25519KeyGenSpec::defaultSpec); Ed25519KeyGenSpec::defaultSpec);
registerAsymmetricKeyBuilder(Ed25519PublicKeySpec.class, new Ed25519PublicKeyBuilder(), null); registerPublicKeyImporter(Ed25519PublicKeySpec.class, new Ed25519PublicKeyBuilder());
registerAsymmetricKeyBuilder(Ed25519PrivateKeySpec.class, new Ed25519PrivateKeyBuilder(), null); registerPrivateKeyImporter(Ed25519PrivateKeySpec.class, new Ed25519PrivateKeyBuilder());
} }
} }

View File

@@ -38,7 +38,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEdDSAKeyGenBuilder;
/** /**
* <h2>Key-pair builder for Ed25519</h2> * <h2>Key-pair builder for Ed25519</h2>
* *
* Concrete {@link zeroecho.core.spi.AsymmetricKeyBuilder} implementation for * Concrete {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} implementation for
* generating Ed25519 key pairs. * generating Ed25519 key pairs.
* *
* <p> * <p>
@@ -50,7 +50,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEdDSAKeyGenBuilder;
* <h2>Usage example</h2> <pre>{@code * <h2>Usage example</h2> <pre>{@code
* // Generate a new Ed25519 key pair with default parameters * // Generate a new Ed25519 key pair with default parameters
* Ed25519KeyGenSpec spec = Ed25519KeyGenSpec.defaultSpec(); * Ed25519KeyGenSpec spec = Ed25519KeyGenSpec.defaultSpec();
* KeyPair kp = CryptoAlgorithms.keyPair("Ed25519", spec); * KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("Ed25519", spec);
* }</pre> * }</pre>
* *
* <h2>Thread-safety</h2> Instances of this builder are stateless and may be * <h2>Thread-safety</h2> Instances of this builder are stateless and may be

View File

@@ -50,7 +50,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* <h2>Usage example</h2> <pre>{@code * <h2>Usage example</h2> <pre>{@code
* // Generate a new Ed25519 key pair using the default spec * // Generate a new Ed25519 key pair using the default spec
* KeyPair kp = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec()); * KeyPair kp = session.keyBuilders().asymmetric()
* .generateKeyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
* }</pre> * }</pre>
* *
* <h2>Thread-safety</h2> The default spec instance is immutable and safe to * <h2>Thread-safety</h2> The default spec instance is immutable and safe to

View File

@@ -38,7 +38,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder;
/** /**
* <h2>Private key builder for Ed25519</h2> * <h2>Private key builder for Ed25519</h2>
* *
* Concrete {@link zeroecho.core.spi.AsymmetricKeyBuilder} for importing and * Concrete {@link zeroecho.core.spi.PrivateKeyImporter} for importing
* wrapping Ed25519 private keys. * wrapping Ed25519 private keys.
* *
* <p> * <p>
@@ -59,7 +59,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder;
* <h2>Usage example</h2> <pre>{@code * <h2>Usage example</h2> <pre>{@code
* // Import a private key from its encoded PKCS#8 form * // Import a private key from its encoded PKCS#8 form
* Ed25519PrivateKeySpec spec = new Ed25519PrivateKeySpec(pkcs8Bytes); * Ed25519PrivateKeySpec spec = new Ed25519PrivateKeySpec(pkcs8Bytes);
* PrivateKey privateKey = CryptoAlgorithms.privateKey("Ed25519", spec); * PrivateKey privateKey = session.keyBuilders().asymmetric().importPrivate("Ed25519", spec);
* }</pre> * }</pre>
* *
* <h2>Thread-safety</h2> Instances of this builder are stateless and may be * <h2>Thread-safety</h2> Instances of this builder are stateless and may be

View File

@@ -33,7 +33,11 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.core.alg.ed25519; package zeroecho.core.alg.ed25519;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.concurrent.locks.ReentrantLock;
import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq;
import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.AlgorithmKeySpec;
@@ -65,7 +69,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* Ed25519PrivateKeySpec spec = new Ed25519PrivateKeySpec(pkcs8Bytes); * Ed25519PrivateKeySpec spec = new Ed25519PrivateKeySpec(pkcs8Bytes);
* *
* // Import into a PrivateKey using ZeroEcho * // 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) * // Serialize to PairSeq (e.g., for configuration or transport)
* PairSeq seq = Ed25519PrivateKeySpec.marshal(spec); * PairSeq seq = Ed25519PrivateKeySpec.marshal(spec);
@@ -74,16 +78,17 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* Ed25519PrivateKeySpec restored = Ed25519PrivateKeySpec.unmarshal(seq); * Ed25519PrivateKeySpec restored = Ed25519PrivateKeySpec.unmarshal(seq);
* }</pre> * }</pre>
* *
* <h2>Thread-safety</h2> Instances are immutable. The internal key bytes are * <h2>Thread-safety</h2> Access and destruction are synchronized. The internal
* defensively copied on construction and retrieval, making this class safe to * key bytes are defensively copied on construction and retrieval.
* share across threads.
* *
* @since 1.0 * @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 static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] encodedPkcs8; 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. * 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 * @return clone of the PKCS#8 encoding
*/ */
public byte[] encoded() { 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} * @return serialized representation as a {@link PairSeq}
*/ */
public static PairSeq marshal(Ed25519PrivateKeySpec spec) { 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); return PairSeq.of("type", "Ed25519-PRIV", PKCS8_B64, b64);
} }
@@ -143,12 +154,62 @@ public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec {
String k = cur.key(); String k = cur.key();
String v = cur.value(); String v = cur.value();
if (PKCS8_B64.equals(k)) { if (PKCS8_B64.equals(k)) {
out = Base64.getDecoder().decode(v); out = decodeReplacing(out, v);
} }
} }
if (out == null) { if (out == null) {
throw new IllegalArgumentException("pkcs8.b64 missing for Ed25519 private key"); throw new IllegalArgumentException("pkcs8.b64 missing for Ed25519 private key");
} }
return new Ed25519PrivateKeySpec(out); try {
return new Ed25519PrivateKeySpec(out);
} finally {
Arrays.fill(out, (byte) 0);
}
}
private static byte[] decodeReplacing(byte[] current, String encoded) {
if (current != null) {
Arrays.fill(current, (byte) 0);
}
return Base64.getDecoder().decode(encoded);
}
private String encodedKey() {
lifecycleLock.lock();
try {
ensureActive();
return Base64.getEncoder().withoutPadding().encodeToString(encodedPkcs8);
} finally {
lifecycleLock.unlock();
}
}
@Override
public void destroy() {
lifecycleLock.lock();
try {
if (!destroyed) {
Arrays.fill(encodedPkcs8, (byte) 0);
destroyed = true;
}
} finally {
lifecycleLock.unlock();
}
}
@Override
public boolean isDestroyed() {
lifecycleLock.lock();
try {
return destroyed;
} finally {
lifecycleLock.unlock();
}
}
private void ensureActive() {
if (destroyed) {
throw new IllegalStateException("Ed25519 private key specification has been destroyed");
}
} }
} }

View File

@@ -38,7 +38,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder;
/** /**
* <h2>Public key builder for Ed25519</h2> * <h2>Public key builder for Ed25519</h2>
* *
* Concrete {@link zeroecho.core.spi.AsymmetricKeyBuilder} for importing and * Concrete {@link zeroecho.core.spi.PublicKeyImporter} for importing
* wrapping Ed25519 public keys. * wrapping Ed25519 public keys.
* *
* <p> * <p>
@@ -59,7 +59,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder;
* <h2>Usage example</h2> <pre>{@code * <h2>Usage example</h2> <pre>{@code
* // Import a public key from its encoded X.509 form * // Import a public key from its encoded X.509 form
* Ed25519PublicKeySpec spec = new Ed25519PublicKeySpec(x509Bytes); * Ed25519PublicKeySpec spec = new Ed25519PublicKeySpec(x509Bytes);
* PublicKey publicKey = CryptoAlgorithms.publicKey("Ed25519", spec); * PublicKey publicKey = session.keyBuilders().asymmetric().importPublic("Ed25519", spec);
* }</pre> * }</pre>
* *
* <h2>Thread-safety</h2> Instances of this builder are stateless and may be * <h2>Thread-safety</h2> Instances of this builder are stateless and may be

View File

@@ -65,7 +65,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* Ed25519PublicKeySpec spec = new Ed25519PublicKeySpec(x509Bytes); * Ed25519PublicKeySpec spec = new Ed25519PublicKeySpec(x509Bytes);
* *
* // Import into a PublicKey using ZeroEcho * // 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) * // Serialize to PairSeq (e.g., for configuration or transport)
* PairSeq seq = Ed25519PublicKeySpec.marshal(spec); * PairSeq seq = Ed25519PublicKeySpec.marshal(spec);

View File

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

View File

@@ -82,12 +82,13 @@ import zeroecho.core.spec.VoidSpec;
* <pre>{@code * <pre>{@code
* // Example: generate a key pair and sign data * // Example: generate a key pair and sign data
* CryptoAlgorithm ed448 = new Ed448Algorithm(); * 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); * 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); * boolean ok = verifier.verify(data, sig);
* }</pre> * }</pre>
* *
@@ -128,7 +129,7 @@ public final class Ed448Algorithm extends AbstractCryptoAlgorithm {
* <pre>{@code * <pre>{@code
* // Example: instantiate and obtain a signer * // Example: instantiate and obtain a signer
* CryptoAlgorithm ed448 = new Ed448Algorithm(); * CryptoAlgorithm ed448 = new Ed448Algorithm();
* SignatureContext signer = ed448.create(KeyUsage.SIGN, privateKey, null); * SignatureContext signer = ed448.createContext(KeyUsage.SIGN, privateKey, null);
* }</pre> * }</pre>
*/ */
public Ed448Algorithm() { public Ed448Algorithm() {
@@ -155,8 +156,9 @@ public final class Ed448Algorithm extends AbstractCryptoAlgorithm {
}, () -> VoidSpec.INSTANCE); }, () -> VoidSpec.INSTANCE);
// Key builders // Key builders
registerAsymmetricKeyBuilder(Ed448KeyGenSpec.class, new Ed448KeyGenBuilder(), Ed448KeyGenSpec::defaultSpec); registerAsymmetricKeyPairGenerator(Ed448KeyGenSpec.class, new Ed448KeyGenBuilder(),
registerAsymmetricKeyBuilder(Ed448PublicKeySpec.class, new Ed448PublicKeyBuilder(), null); Ed448KeyGenSpec::defaultSpec);
registerAsymmetricKeyBuilder(Ed448PrivateKeySpec.class, new Ed448PrivateKeyBuilder(), null); registerPublicKeyImporter(Ed448PublicKeySpec.class, new Ed448PublicKeyBuilder());
registerPrivateKeyImporter(Ed448PrivateKeySpec.class, new Ed448PrivateKeyBuilder());
} }
} }

View File

@@ -35,7 +35,6 @@ package zeroecho.core.alg.ed448;
import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder; import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder;
import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.AlgorithmKeySpec;
import zeroecho.core.spi.AsymmetricKeyBuilder;
/** /**
* <h2>Ed448 Private Key Builder</h2> * <h2>Ed448 Private Key Builder</h2>
@@ -66,8 +65,8 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* }</pre> * }</pre>
* *
* <h2>Thread-safety</h2> Stateless and safe for concurrent use. Each call to * <h2>Thread-safety</h2> Stateless and safe for concurrent use. Each call to
* {@link AsymmetricKeyBuilder#importPrivate(AlgorithmKeySpec) * {@link zeroecho.core.spi.PrivateKeyImporter#importPrivate(AlgorithmKeySpec)}
* importPrivate(Ed448PrivateKeySpec)} creates a new * creates a new
* {@link java.security.KeyFactory}. * {@link java.security.KeyFactory}.
* *
* @since 1.0 * @since 1.0

View File

@@ -33,7 +33,11 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.core.alg.ed448; package zeroecho.core.alg.ed448;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.concurrent.locks.ReentrantLock;
import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq;
import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.AlgorithmKeySpec;
@@ -41,7 +45,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
/** /**
* <h2>Ed448 Private Key Specification</h2> * <h2>Ed448 Private Key Specification</h2>
* *
* Immutable specification for an Ed448 private key in PKCS#8 encoding. * Destroyable specification for an Ed448 private key in PKCS#8 encoding.
* *
* <p> * <p>
* This class acts as a typed carrier for encoded private key material, * 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); * Ed448PrivateKeySpec restored = Ed448PrivateKeySpec.unmarshal(p);
* }</pre> * }</pre>
* *
* <h2>Thread-safety</h2> Instances are immutable and safe to share across * <h2>Thread-safety</h2> Access and destruction are synchronized.
* threads.
* *
* @since 1.0 * @since 1.0
*/ */
public final class Ed448PrivateKeySpec implements AlgorithmKeySpec { public final class Ed448PrivateKeySpec implements AlgorithmKeySpec, Destroyable {
private static final String PKCS8_B64 = "pkcs8.b64"; private static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] encodedPkcs8; private final byte[] encodedPkcs8;
private final ReentrantLock lifecycleLock = new ReentrantLock();
private boolean destroyed;
/** /**
* Constructs a new Ed448 private key spec from the given PKCS#8-encoded bytes. * Constructs a new Ed448 private key spec from the given PKCS#8-encoded bytes.
@@ -103,7 +108,13 @@ public final class Ed448PrivateKeySpec implements AlgorithmKeySpec {
* @return cloned PKCS#8-encoded key bytes * @return cloned PKCS#8-encoded key bytes
*/ */
public byte[] encoded() { public byte[] encoded() {
return encodedPkcs8.clone(); lifecycleLock.lock();
try {
ensureActive();
return encodedPkcs8.clone();
} finally {
lifecycleLock.unlock();
}
} }
/** /**
@@ -119,7 +130,7 @@ public final class Ed448PrivateKeySpec implements AlgorithmKeySpec {
* @return a {@link PairSeq} containing the type and base64 data * @return a {@link PairSeq} containing the type and base64 data
*/ */
public static PairSeq marshal(Ed448PrivateKeySpec spec) { public static PairSeq marshal(Ed448PrivateKeySpec spec) {
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8); String b64 = spec.encodedKey();
return PairSeq.of("type", "Ed448-PRIV", PKCS8_B64, b64); return PairSeq.of("type", "Ed448-PRIV", PKCS8_B64, b64);
} }
@@ -142,12 +153,62 @@ public final class Ed448PrivateKeySpec implements AlgorithmKeySpec {
String k = cur.key(); String k = cur.key();
String v = cur.value(); String v = cur.value();
if (PKCS8_B64.equals(k)) { if (PKCS8_B64.equals(k)) {
out = Base64.getDecoder().decode(v); out = decodeReplacing(out, v);
} }
} }
if (out == null) { if (out == null) {
throw new IllegalArgumentException("pkcs8.b64 missing for Ed448 private key"); throw new IllegalArgumentException("pkcs8.b64 missing for Ed448 private key");
} }
return new Ed448PrivateKeySpec(out); try {
return new Ed448PrivateKeySpec(out);
} finally {
Arrays.fill(out, (byte) 0);
}
}
private static byte[] decodeReplacing(byte[] current, String encoded) {
if (current != null) {
Arrays.fill(current, (byte) 0);
}
return Base64.getDecoder().decode(encoded);
}
private String encodedKey() {
lifecycleLock.lock();
try {
ensureActive();
return Base64.getEncoder().withoutPadding().encodeToString(encodedPkcs8);
} finally {
lifecycleLock.unlock();
}
}
@Override
public void destroy() {
lifecycleLock.lock();
try {
if (!destroyed) {
Arrays.fill(encodedPkcs8, (byte) 0);
destroyed = true;
}
} finally {
lifecycleLock.unlock();
}
}
@Override
public boolean isDestroyed() {
lifecycleLock.lock();
try {
return destroyed;
} finally {
lifecycleLock.unlock();
}
}
private void ensureActive() {
if (destroyed) {
throw new IllegalStateException("Ed448 private key specification has been destroyed");
}
} }
} }

View File

@@ -35,7 +35,6 @@ package zeroecho.core.alg.ed448;
import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder; import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder;
import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.AlgorithmKeySpec;
import zeroecho.core.spi.AsymmetricKeyBuilder;
/** /**
* <h2>Ed448 Public Key Builder</h2> * <h2>Ed448 Public Key Builder</h2>
@@ -65,8 +64,8 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* }</pre> * }</pre>
* *
* <h2>Thread-safety</h2> Stateless and safe for concurrent use. Each call to * <h2>Thread-safety</h2> Stateless and safe for concurrent use. Each call to
* {@link AsymmetricKeyBuilder#importPublic(AlgorithmKeySpec) * {@link zeroecho.core.spi.PublicKeyImporter#importPublic(AlgorithmKeySpec)}
* importPublic(Ed448PublicKeySpec)} creates a new * creates a new
* {@link java.security.KeyFactory}. * {@link java.security.KeyFactory}.
* *
* @since 1.0 * @since 1.0

View File

@@ -38,7 +38,8 @@
* This package wires the Ed448 Edwards-curve Digital Signature Algorithm into * This package wires the Ed448 Edwards-curve Digital Signature Algorithm into
* the core layer. It provides the algorithm descriptor, a streaming signature * the core layer. It provides the algorithm descriptor, a streaming signature
* context with a fixed 114-byte tag length, builders for generating and * context with a fixed 114-byte tag length, builders for generating and
* importing keys, and immutable key specifications with marshalling helpers. * importing keys, and defensively copying key specifications with marshalling
* helpers.
* </p> * </p>
* *
* <h2>Scope and responsibilities</h2> * <h2>Scope and responsibilities</h2>
@@ -49,8 +50,8 @@
* enforces the 114-byte tag size.</li> * enforces the 114-byte tag size.</li>
* <li>Expose builders for key-pair generation and for importing encoded * <li>Expose builders for key-pair generation and for importing encoded
* keys.</li> * keys.</li>
* <li>Define immutable key specifications suitable for safe cloning and simple * <li>Define defensively copying key specifications suitable for safe cloning
* marshalling.</li> * and simple marshalling; private-key specifications are destroyable.</li>
* </ul> * </ul>
* *
* <h2>Components</h2> * <h2>Components</h2>
@@ -63,9 +64,9 @@
* marker spec for producing key pairs.</li> * marker spec for producing key pairs.</li>
* <li><b>Ed448PublicKeyBuilder</b> / <b>Ed448PrivateKeyBuilder</b>: importers * <li><b>Ed448PublicKeyBuilder</b> / <b>Ed448PrivateKeyBuilder</b>: importers
* backed by JCA key factories.</li> * backed by JCA key factories.</li>
* <li><b>Ed448PublicKeySpec</b> / <b>Ed448PrivateKeySpec</b>: immutable * <li><b>Ed448PublicKeySpec</b> / <b>Ed448PrivateKeySpec</b>: wrappers over
* wrappers over X.509 and PKCS#8 encodings, with defensive copying and base64 * X.509 and PKCS#8 encodings, with defensive copying and base64 marshalling
* marshalling helpers.</li> * helpers; the private-key form is destroyable.</li>
* </ul> * </ul>
* *
* <h2>Design notes</h2> * <h2>Design notes</h2>

View File

@@ -47,6 +47,7 @@ import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import org.bouncycastle.jce.spec.ElGamalParameterSpec; import org.bouncycastle.jce.spec.ElGamalParameterSpec;
@@ -54,7 +55,9 @@ import zeroecho.core.AlgorithmFamily;
import zeroecho.core.KeyUsage; import zeroecho.core.KeyUsage;
import zeroecho.core.alg.AbstractCryptoAlgorithm; import zeroecho.core.alg.AbstractCryptoAlgorithm;
import zeroecho.core.context.EncryptionContext; import zeroecho.core.context.EncryptionContext;
import zeroecho.core.spi.AsymmetricKeyBuilder; import zeroecho.core.spi.AsymmetricKeyPairGenerator;
import zeroecho.core.spi.PrivateKeyImporter;
import zeroecho.core.spi.PublicKeyImporter;
/** /**
* <h2>ElGamal Asymmetric Encryption Algorithm</h2> * <h2>ElGamal Asymmetric Encryption Algorithm</h2>
@@ -110,11 +113,12 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* *
* <h2>Example</h2> <pre>{@code * <h2>Example</h2> <pre>{@code
* CryptoAlgorithm algo = new ElgamalAlgorithm(); * 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()); * ElgamalEncSpec.pkcs1());
* EncryptionContext dec = algo.create(KeyUsage.DECRYPT, kp.getPrivate(), * EncryptionContext dec = algo.createContext(KeyUsage.DECRYPT, kp.getPrivate(),
* ElgamalEncSpec.pkcs1()); * ElgamalEncSpec.pkcs1());
* }</pre> * }</pre>
* *
@@ -148,7 +152,7 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm {
if (false) { // NOPMD if (false) { // NOPMD
// this key generation is slow // this key generation is slow
registerAsymmetricKeyBuilder(ElgamalKeyGenSpec.class, new AsymmetricKeyBuilder<>() { registerAsymmetricKeyPairGenerator(ElgamalKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
@Override @Override
public KeyPair generateKeyPair(ElgamalKeyGenSpec spec) throws GeneralSecurityException { public KeyPair generateKeyPair(ElgamalKeyGenSpec spec) throws GeneralSecurityException {
ensureBC(); ensureBC();
@@ -161,20 +165,10 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm {
kpg.initialize(eg, new SecureRandom()); kpg.initialize(eg, new SecureRandom());
return kpg.generateKeyPair(); 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); }, ElgamalKeyGenSpec::elgamal2048);
} }
registerAsymmetricKeyBuilder(ElgamalParamSpec.class, new AsymmetricKeyBuilder<>() { registerAsymmetricKeyPairGenerator(ElgamalParamSpec.class, new AsymmetricKeyPairGenerator<>() {
@Override @Override
public KeyPair generateKeyPair(ElgamalParamSpec spec) throws GeneralSecurityException { public KeyPair generateKeyPair(ElgamalParamSpec spec) throws GeneralSecurityException {
ensureBC(); ensureBC();
@@ -183,23 +177,9 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm {
kpg.initialize(eg, new SecureRandom()); kpg.initialize(eg, new SecureRandom());
return kpg.generateKeyPair(); 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); }, ElgamalParamSpec::ffdhe2048);
registerAsymmetricKeyBuilder(ElgamalPublicKeySpec.class, new AsymmetricKeyBuilder<>() { registerPublicKeyImporter(ElgamalPublicKeySpec.class, new PublicKeyImporter<>() {
@Override
public KeyPair generateKeyPair(ElgamalPublicKeySpec spec) {
throw new UnsupportedOperationException("Generation not supported for encoded spec.");
}
@Override @Override
public PublicKey importPublic(ElgamalPublicKeySpec spec) throws GeneralSecurityException { public PublicKey importPublic(ElgamalPublicKeySpec spec) throws GeneralSecurityException {
@@ -207,31 +187,22 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm {
KeyFactory kf = KeyFactory.getInstance(EL_GAMAL, providerName()); KeyFactory kf = KeyFactory.getInstance(EL_GAMAL, providerName());
return kf.generatePublic(new X509EncodedKeySpec(spec.encoded())); return kf.generatePublic(new X509EncodedKeySpec(spec.encoded()));
} }
});
@Override registerPrivateKeyImporter(ElgamalPrivateKeySpec.class, new PrivateKeyImporter<>() {
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.");
}
@Override @Override
public PrivateKey importPrivate(ElgamalPrivateKeySpec spec) throws GeneralSecurityException { public PrivateKey importPrivate(ElgamalPrivateKeySpec spec) throws GeneralSecurityException {
ensureBC(); ensureBC();
KeyFactory kf = KeyFactory.getInstance(EL_GAMAL, providerName()); KeyFactory kf = KeyFactory.getInstance(EL_GAMAL, providerName());
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); byte[] encoded = spec.encoded();
try {
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
} finally {
Arrays.fill(encoded, (byte) 0);
}
} }
}, null); });
} }
/** /**

View File

@@ -137,7 +137,7 @@ public final class ElgamalCipherContext implements EncryptionContext {
return CipherTransformInputStreamBuilder.builder().withCipher(cipher).withUpstream(upstream) return CipherTransformInputStreamBuilder.builder().withCipher(cipher).withUpstream(upstream)
.withInputBlockSize(g.inputBlockSize()).withOutputBlockSize(g.perBlockOutput()) .withInputBlockSize(g.inputBlockSize()).withOutputBlockSize(g.perBlockOutput())
.withLeftZeroPadding(g.noPadding).build(); .withLeftZeroPadding(g.noPadding).withIndependentBlocks().build();
} }
/** /**

View File

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

View File

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

View File

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

View File

@@ -33,7 +33,11 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.core.alg.elgamal; package zeroecho.core.alg.elgamal;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.concurrent.locks.ReentrantLock;
import javax.security.auth.Destroyable;
import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq;
import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.AlgorithmKeySpec;
@@ -62,7 +66,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* // Import from PKCS#8 DER * // Import from PKCS#8 DER
* byte[] der = Files.readAllBytes(Path.of("elgamal-priv.der")); * byte[] der = Files.readAllBytes(Path.of("elgamal-priv.der"));
* ElgamalPrivateKeySpec spec = new ElgamalPrivateKeySpec(der); * ElgamalPrivateKeySpec spec = new ElgamalPrivateKeySpec(der);
* PrivateKey priv = algo.importPrivate(spec); * PrivateKey priv = algo.privateKeyImporter(ElgamalPrivateKeySpec.class).importPrivate(spec);
* *
* // Marshal for serialization * // Marshal for serialization
* PairSeq ps = ElgamalPrivateKeySpec.marshal(spec); * PairSeq ps = ElgamalPrivateKeySpec.marshal(spec);
@@ -79,15 +83,16 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* handling when possible.</li> * handling when possible.</li>
* </ul> * </ul>
* *
* <h2>Thread-safety</h2> Instances are immutable and safe to share between * <h2>Thread-safety</h2> Access and destruction are synchronized.
* threads.
* *
* @since 1.0 * @since 1.0
*/ */
public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec { public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
private static final String PKCS8_B64 = "pkcs8.b64"; private static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] pkcs8; private final byte[] pkcs8;
private final ReentrantLock lifecycleLock = new ReentrantLock();
private boolean destroyed;
/** /**
* Constructs a new private key spec from PKCS#8-encoded bytes. * Constructs a new private key spec from PKCS#8-encoded bytes.
@@ -104,7 +109,13 @@ public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec {
* @return PKCS#8 DER encoding * @return PKCS#8 DER encoding
*/ */
public byte[] encoded() { public byte[] encoded() {
return pkcs8.clone(); lifecycleLock.lock();
try {
ensureActive();
return pkcs8.clone();
} finally {
lifecycleLock.unlock();
}
} }
/** /**
@@ -122,7 +133,7 @@ public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec {
* @return marshalled key as {@link PairSeq} * @return marshalled key as {@link PairSeq}
*/ */
public static PairSeq marshal(ElgamalPrivateKeySpec spec) { public static PairSeq marshal(ElgamalPrivateKeySpec spec) {
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); String b64 = spec.encodedKey();
return PairSeq.of("type", "ELGAMAL-PRIV", PKCS8_B64, b64); return PairSeq.of("type", "ELGAMAL-PRIV", PKCS8_B64, b64);
} }
@@ -141,12 +152,62 @@ public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec {
String k = cur.key(); String k = cur.key();
String v = cur.value(); String v = cur.value();
if (PKCS8_B64.equals(k)) { if (PKCS8_B64.equals(k)) {
out = Base64.getDecoder().decode(v); out = decodeReplacing(out, v);
} }
} }
if (out == null) { if (out == null) {
throw new IllegalArgumentException("pkcs8.b64 missing for ElGamal private key"); throw new IllegalArgumentException("pkcs8.b64 missing for ElGamal private key");
} }
return new ElgamalPrivateKeySpec(out); try {
return new ElgamalPrivateKeySpec(out);
} finally {
Arrays.fill(out, (byte) 0);
}
}
private static byte[] decodeReplacing(byte[] current, String encoded) {
if (current != null) {
Arrays.fill(current, (byte) 0);
}
return Base64.getDecoder().decode(encoded);
}
private String encodedKey() {
lifecycleLock.lock();
try {
ensureActive();
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
} finally {
lifecycleLock.unlock();
}
}
@Override
public void destroy() {
lifecycleLock.lock();
try {
if (!destroyed) {
Arrays.fill(pkcs8, (byte) 0);
destroyed = true;
}
} finally {
lifecycleLock.unlock();
}
}
@Override
public boolean isDestroyed() {
lifecycleLock.lock();
try {
return destroyed;
} finally {
lifecycleLock.unlock();
}
}
private void ensureActive() {
if (destroyed) {
throw new IllegalStateException("ElGamal private key specification has been destroyed");
}
} }
} }

View File

@@ -62,7 +62,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* // Import from X.509 DER * // Import from X.509 DER
* byte[] der = Files.readAllBytes(Path.of("elgamal-pub.der")); * byte[] der = Files.readAllBytes(Path.of("elgamal-pub.der"));
* ElgamalPublicKeySpec spec = new ElgamalPublicKeySpec(der); * ElgamalPublicKeySpec spec = new ElgamalPublicKeySpec(der);
* PublicKey pub = algo.importPublic(spec); * PublicKey pub = algo.publicKeyImporter(ElgamalPublicKeySpec.class).importPublic(spec);
* *
* // Marshal for serialization * // Marshal for serialization
* PairSeq ps = ElgamalPublicKeySpec.marshal(spec); * PairSeq ps = ElgamalPublicKeySpec.marshal(spec);

View File

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

View File

@@ -44,6 +44,7 @@ import java.security.PublicKey;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; 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.KemContext;
import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.context.MessageAgreementContext;
import zeroecho.core.spec.VoidSpec; import zeroecho.core.spec.VoidSpec;
import zeroecho.core.spi.AsymmetricKeyBuilder; import zeroecho.core.spi.AsymmetricKeyPairGenerator;
import zeroecho.core.spi.PrivateKeyImporter;
import zeroecho.core.spi.PublicKeyImporter;
/** /**
* <h2>Frodo Key Encapsulation Mechanism (KEM)</h2> * <h2>Frodo Key Encapsulation Mechanism (KEM)</h2>
@@ -104,9 +107,8 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* X.509 encoding.</li> * X.509 encoding.</li>
* <li>Private keys may be imported from {@link FrodoPrivateKeySpec} using a * <li>Private keys may be imported from {@link FrodoPrivateKeySpec} using a
* PKCS#8 encoding.</li> * PKCS#8 encoding.</li>
* <li>Direct import of key specs via {@code generateKeyPair} in the spec-based * <li>Generation and public/private import are registered as independent exact
* builders is not supported and will throw * capabilities.</li>
* {@link UnsupportedOperationException}.</li>
* </ul> * </ul>
* *
* <h2>Provider requirements</h2> * <h2>Provider requirements</h2>
@@ -120,13 +122,14 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* <h2>Example usage</h2> <pre>{@code * <h2>Example usage</h2> <pre>{@code
* // Generate a Frodo keypair * // Generate a Frodo keypair
* CryptoAlgorithm frodo = CryptoAlgorithms.require("Frodo"); * 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 * // 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 * // Decapsulate using the recipient's private key
* KemContext dec = frodo.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE); * KemContext dec = frodo.createContext(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
* }</pre> * }</pre>
* *
* <h2>Thread-safety</h2> * <h2>Thread-safety</h2>
@@ -201,7 +204,7 @@ public final class FrodoAlgorithm extends AbstractCryptoAlgorithm {
.build(); .build();
}, () -> VoidSpec.INSTANCE); }, () -> VoidSpec.INSTANCE);
registerAsymmetricKeyBuilder(FrodoKeyGenSpec.class, new AsymmetricKeyBuilder<>() { registerAsymmetricKeyPairGenerator(FrodoKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
@Override @Override
public KeyPair generateKeyPair(FrodoKeyGenSpec spec) throws GeneralSecurityException { public KeyPair generateKeyPair(FrodoKeyGenSpec spec) throws GeneralSecurityException {
ensureProvider(); ensureProvider();
@@ -217,23 +220,9 @@ public final class FrodoAlgorithm extends AbstractCryptoAlgorithm {
kpg.initialize(params, new SecureRandom()); kpg.initialize(params, new SecureRandom());
return kpg.generateKeyPair(); return kpg.generateKeyPair();
} }
@Override
public PublicKey importPublic(FrodoKeyGenSpec spec) {
throw new UnsupportedOperationException();
}
@Override
public PrivateKey importPrivate(FrodoKeyGenSpec spec) {
throw new UnsupportedOperationException();
}
}, FrodoKeyGenSpec::frodo1344aes); }, FrodoKeyGenSpec::frodo1344aes);
registerAsymmetricKeyBuilder(FrodoPublicKeySpec.class, new AsymmetricKeyBuilder<>() { registerPublicKeyImporter(FrodoPublicKeySpec.class, new PublicKeyImporter<>() {
@Override
public KeyPair generateKeyPair(FrodoPublicKeySpec spec) {
throw new UnsupportedOperationException();
}
@Override @Override
public PublicKey importPublic(FrodoPublicKeySpec spec) throws GeneralSecurityException { public PublicKey importPublic(FrodoPublicKeySpec spec) throws GeneralSecurityException {
@@ -241,31 +230,22 @@ public final class FrodoAlgorithm extends AbstractCryptoAlgorithm {
KeyFactory kf = KeyFactory.getInstance("Frodo", providerName()); KeyFactory kf = KeyFactory.getInstance("Frodo", providerName());
return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
} }
});
@Override registerPrivateKeyImporter(FrodoPrivateKeySpec.class, new PrivateKeyImporter<>() {
public PrivateKey importPrivate(FrodoPublicKeySpec spec) {
throw new UnsupportedOperationException();
}
}, null);
registerAsymmetricKeyBuilder(FrodoPrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
@Override
public KeyPair generateKeyPair(FrodoPrivateKeySpec spec) {
throw new UnsupportedOperationException();
}
@Override
public PublicKey importPublic(FrodoPrivateKeySpec spec) {
throw new UnsupportedOperationException();
}
@Override @Override
public PrivateKey importPrivate(FrodoPrivateKeySpec spec) throws GeneralSecurityException { public PrivateKey importPrivate(FrodoPrivateKeySpec spec) throws GeneralSecurityException {
ensureProvider(); ensureProvider();
KeyFactory kf = KeyFactory.getInstance("Frodo", providerName()); KeyFactory kf = KeyFactory.getInstance("Frodo", providerName());
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); byte[] encoded = spec.pkcs8();
try {
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
} finally {
Arrays.fill(encoded, (byte) 0);
}
} }
}, null); });
} }
private static void ensureProvider() throws NoSuchProviderException { private static void ensureProvider() throws NoSuchProviderException {

View File

@@ -33,10 +33,13 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.core.alg.frodo; package zeroecho.core.alg.frodo;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
import javax.security.auth.Destroyable;
import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq;
import zeroecho.core.marshal.PairSeq.Cursor; import zeroecho.core.marshal.PairSeq.Cursor;
import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.AlgorithmKeySpec;
@@ -45,7 +48,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <h2>Specification for importing a Frodo private key</h2> * <h2>Specification for importing a Frodo private key</h2>
* *
* {@code FrodoPrivateKeySpec} is a simple wrapper around a PKCS#8-encoded * {@code FrodoPrivateKeySpec} is a simple wrapper around a PKCS#8-encoded
* FrodoKEM private key. It provides immutable access to the raw encoding and * FrodoKEM private key. It provides defensive access to the raw encoding and
* utilities for serialization. * utilities for serialization.
* *
* <h2>Encoding format</h2> * <h2>Encoding format</h2>
@@ -59,9 +62,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <h2>Usage</h2> * <h2>Usage</h2>
* <ul> * <ul>
* <li>Instances of this spec can be passed to * <li>Instances of this spec can be passed to
* {@link CryptoAlgorithm#importPrivate(AlgorithmKeySpec) * {@link zeroecho.sdk.KeyBuilders.Asymmetric#importPrivate(String, AlgorithmKeySpec)}
* importPrivate(FrodoPrivateKeySpec)} to construct a usable * to construct a usable {@link java.security.PrivateKey} object.</li>
* {@link java.security.PrivateKey} object.</li>
* <li>The {@link #marshal(FrodoPrivateKeySpec)} and {@link #unmarshal(PairSeq)} * <li>The {@link #marshal(FrodoPrivateKeySpec)} and {@link #unmarshal(PairSeq)}
* helpers allow safe conversion to/from structured textual form for persistence * helpers allow safe conversion to/from structured textual form for persistence
* or transmission.</li> * or transmission.</li>
@@ -71,21 +73,23 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* // Import an existing Frodo private key * // Import an existing Frodo private key
* byte[] encoded = Files.readAllBytes(Paths.get("frodo.key")); * byte[] encoded = Files.readAllBytes(Paths.get("frodo.key"));
* FrodoPrivateKeySpec spec = new FrodoPrivateKeySpec(encoded); * FrodoPrivateKeySpec spec = new FrodoPrivateKeySpec(encoded);
* PrivateKey priv = frodo.importPrivate(spec); * PrivateKey priv = session.keyBuilders().asymmetric().importPrivate("FrodoKEM", spec);
* }</pre> * }</pre>
* *
* <h2>Thread-safety</h2> * <h2>Thread-safety</h2>
* <p> * <p>
* This class is immutable; the internal byte array is cloned on construction * The internal byte array is cloned on construction and when accessed via
* and when accessed via {@link #pkcs8()}. * {@link #pkcs8()}. Access and destruction are synchronized.
* </p> * </p>
* *
* @since 1.0 * @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 static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] pkcs8; private final byte[] pkcs8;
private final ReentrantLock lifecycleLock = new ReentrantLock();
private boolean destroyed;
/** /**
* Creates a new specification from a PKCS#8 DER-encoded key. * 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 * @return defensive copy of the PKCS#8-encoded private key
*/ */
public byte[] pkcs8() { 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 * @return a {@code PairSeq} with type and Base64-encoded key
*/ */
public static PairSeq marshal(FrodoPrivateKeySpec spec) { 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); return PairSeq.of("type", "FrodoPrivateKeySpec", PKCS8_B64, b64);
} }
@@ -136,7 +146,12 @@ public final class FrodoPrivateKeySpec implements AlgorithmKeySpec {
if (b64 == null) { if (b64 == null) {
throw new IllegalArgumentException("FrodoPrivateKeySpec: missing pkcs8.b64"); 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() { public String toString() {
return "FrodoPrivateKeySpec[len=" + pkcs8.length + "]"; return "FrodoPrivateKeySpec[len=" + pkcs8.length + "]";
} }
private String encodedKey() {
lifecycleLock.lock();
try {
ensureActive();
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
} finally {
lifecycleLock.unlock();
}
}
@Override
public void destroy() {
lifecycleLock.lock();
try {
if (!destroyed) {
Arrays.fill(pkcs8, (byte) 0);
destroyed = true;
}
} finally {
lifecycleLock.unlock();
}
}
@Override
public boolean isDestroyed() {
lifecycleLock.lock();
try {
return destroyed;
} finally {
lifecycleLock.unlock();
}
}
private void ensureActive() {
if (destroyed) {
throw new IllegalStateException("Frodo private key specification has been destroyed");
}
}
} }

View File

@@ -36,7 +36,6 @@ package zeroecho.core.alg.frodo;
import java.util.Base64; import java.util.Base64;
import java.util.Objects; import java.util.Objects;
import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq;
import zeroecho.core.marshal.PairSeq.Cursor; import zeroecho.core.marshal.PairSeq.Cursor;
import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.AlgorithmKeySpec;
@@ -59,9 +58,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <h2>Usage</h2> * <h2>Usage</h2>
* <ul> * <ul>
* <li>Instances of this spec can be passed to * <li>Instances of this spec can be passed to
* {@link CryptoAlgorithm#importPublic(AlgorithmKeySpec) * {@link zeroecho.sdk.KeyBuilders.Asymmetric#importPublic(String, AlgorithmKeySpec)}
* importPublic(FrodoPublicKeySpec)} to construct a usable * to construct a usable {@link java.security.PublicKey}.</li>
* {@link java.security.PublicKey}.</li>
* <li>The {@link #marshal(FrodoPublicKeySpec)} and {@link #unmarshal(PairSeq)} * <li>The {@link #marshal(FrodoPublicKeySpec)} and {@link #unmarshal(PairSeq)}
* methods allow safe serialization into and recovery from structured textual * methods allow safe serialization into and recovery from structured textual
* form.</li> * form.</li>
@@ -71,7 +69,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* // Import an existing Frodo public key * // Import an existing Frodo public key
* byte[] encoded = Files.readAllBytes(Paths.get("frodo.pub")); * byte[] encoded = Files.readAllBytes(Paths.get("frodo.pub"));
* FrodoPublicKeySpec spec = new FrodoPublicKeySpec(encoded); * FrodoPublicKeySpec spec = new FrodoPublicKeySpec(encoded);
* PublicKey pub = frodo.importPublic(spec); * PublicKey pub = session.keyBuilders().asymmetric().importPublic("FrodoKEM", spec);
* }</pre> * }</pre>
* *
* <h2>Thread-safety</h2> * <h2>Thread-safety</h2>

View File

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

View File

@@ -33,9 +33,9 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.core.alg.hmac; package zeroecho.core.alg.hmac;
import java.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.KeyGenerator; import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
@@ -45,7 +45,9 @@ import zeroecho.core.AlgorithmFamily;
import zeroecho.core.KeyUsage; import zeroecho.core.KeyUsage;
import zeroecho.core.alg.AbstractCryptoAlgorithm; import zeroecho.core.alg.AbstractCryptoAlgorithm;
import zeroecho.core.context.MacContext; import zeroecho.core.context.MacContext;
import zeroecho.core.spi.SymmetricKeyBuilder; import zeroecho.core.err.ProviderFailureException;
import zeroecho.core.spi.SymmetricKeyGenerator;
import zeroecho.core.spi.SymmetricKeyImporter;
/** /**
* <h2>HMAC Algorithm Integration</h2> * <h2>HMAC Algorithm Integration</h2>
@@ -86,11 +88,11 @@ import zeroecho.core.spi.SymmetricKeyBuilder;
* *
* <h2>Usage example</h2> <pre>{@code * <h2>Usage example</h2> <pre>{@code
* // Generate a fresh key for HMAC-SHA256 * // Generate a fresh key for HMAC-SHA256
* SecretKey key = CryptoAlgorithms.generateSecret("HMAC", * SecretKey key = session.keyBuilders().symmetric().generate("HMAC",
* HmacKeyGenSpec.sha256(256)); * HmacKeyGenSpec.sha256(256));
* *
* // Create a MAC context * // Create a MAC context
* MacContext ctx = CryptoAlgorithms.create("HMAC", * MacContext ctx = session.createContext("HMAC",
* KeyUsage.MAC, key, HmacSpec.sha256()); * KeyUsage.MAC, key, HmacSpec.sha256());
* *
* ctx.update(data); * ctx.update(data);
@@ -133,37 +135,33 @@ public final class HmacAlgorithm extends AbstractCryptoAlgorithm {
try { try {
return new HmacMacContext(this, k, s.macName()); return new HmacMacContext(this, k, s.macName());
} catch (GeneralSecurityException e) { } 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 }, HmacSpec::sha256 // default for catalog/tests
); );
// Key builders (generation/import) — both respect macName in the spec. // Key builders (generation/import) — both respect macName in the spec.
registerSymmetricKeyBuilder(HmacKeyGenSpec.class, new SymmetricKeyBuilder<>() { registerSymmetricKeyGenerator(HmacKeyGenSpec.class, new SymmetricKeyGenerator<>() {
@Override @Override
public SecretKey generateSecret(HmacKeyGenSpec spec) throws GeneralSecurityException { public SecretKey generateSecret(HmacKeyGenSpec spec) throws GeneralSecurityException {
KeyGenerator kg = KeyGenerator.getInstance(spec.macName()); KeyGenerator kg = KeyGenerator.getInstance(spec.macName());
kg.init(spec.keySizeBits(), new SecureRandom()); kg.init(spec.keySizeBits(), new SecureRandom());
return kg.generateKey(); return kg.generateKey();
} }
@Override
public SecretKey importSecret(HmacKeyGenSpec spec) {
throw new UnsupportedOperationException("Use HmacKeyImportSpec for import");
}
}, () -> HmacKeyGenSpec.sha256(256) // default keygen spec }, () -> HmacKeyGenSpec.sha256(256) // default keygen spec
); );
registerSymmetricKeyBuilder(HmacKeyImportSpec.class, new SymmetricKeyBuilder<>() { registerSymmetricKeyImporter(HmacKeyImportSpec.class, new SymmetricKeyImporter<>() {
@Override
public SecretKey generateSecret(HmacKeyImportSpec spec) {
throw new UnsupportedOperationException("Use HmacKeyGenSpec for generation");
}
@Override @Override
public SecretKey importSecret(HmacKeyImportSpec spec) { public SecretKey importSecret(HmacKeyImportSpec spec) {
return new SecretKeySpec(spec.key(), spec.macName()); byte[] key = spec.key();
try {
return new SecretKeySpec(key, spec.macName());
} finally {
Arrays.fill(key, (byte) 0);
}
} }
}, null); });
} }
} }

View File

@@ -55,12 +55,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* <h2>Usage</h2> Typical usage is to construct a spec with a given digest * <h2>Usage</h2> Typical usage is to construct a spec with a given digest
* family and key size, and then pass it to a registered * family and key size, and then pass it to a registered
* {@code SymmetricKeyBuilder}: * {@link zeroecho.core.spi.SymmetricKeyGenerator}:
* *
* <pre>{@code * <pre>{@code
* // Generate a 256-bit key for HMAC-SHA256 * // Generate a 256-bit key for HMAC-SHA256
* HmacKeyGenSpec spec = HmacKeyGenSpec.sha256(256); * HmacKeyGenSpec spec = HmacKeyGenSpec.sha256(256);
* SecretKey key = CryptoAlgorithms.generateSecret("HMAC", spec); * SecretKey key = session.keyBuilders().symmetric().generate("HMAC", spec);
* }</pre> * }</pre>
* *
* <h2>Defaults</h2> Convenience static factories are provided for the most * <h2>Defaults</h2> Convenience static factories are provided for the most

View File

@@ -34,8 +34,12 @@
package zeroecho.core.alg.hmac; package zeroecho.core.alg.hmac;
import java.util.Base64; import java.util.Base64;
import java.util.Arrays;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
import javax.security.auth.Destroyable;
import zeroecho.core.annotation.Describable; import zeroecho.core.annotation.Describable;
import zeroecho.core.marshal.PairSeq; import zeroecho.core.marshal.PairSeq;
@@ -66,7 +70,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* {@code * {@code
* byte[] rawKey = Files.readAllBytes(Paths.get("hmac.key")); * byte[] rawKey = Files.readAllBytes(Paths.get("hmac.key"));
* HmacKeyImportSpec spec = HmacKeyImportSpec.fromRaw("HmacSHA256", rawKey); * HmacKeyImportSpec spec = HmacKeyImportSpec.fromRaw("HmacSHA256", rawKey);
* SecretKey key = CryptoAlgorithms.importSecret("HMAC", spec); * SecretKey key = session.keyBuilders().symmetric().importKey("HMAC", spec);
* } * }
* </pre> * </pre>
* *
@@ -100,9 +104,11 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* @since 1.0 * @since 1.0
*/ */
public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable { public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable, Destroyable {
private final String macName; private final String macName;
private final byte[] key; private final byte[] key;
private final ReentrantLock lifecycleLock = new ReentrantLock();
private boolean destroyed;
/** /**
* Constructs a new HMAC key import specification. * Constructs a new HMAC key import specification.
@@ -132,7 +138,13 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable {
* @return cloned key bytes * @return cloned key bytes
*/ */
public byte[] key() { 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) { public static HmacKeyImportSpec fromHex(String macName, String hex) {
Objects.requireNonNull(hex, "hex must not be null"); 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) { public static HmacKeyImportSpec fromBase64(String macName, String b64) {
Objects.requireNonNull(b64, "base64 must not be null"); 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 * @return encoded key spec sequence
*/ */
public static PairSeq marshal(HmacKeyImportSpec spec) { 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); 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; String mac = null;
byte[] key = null; byte[] key = null;
PairSeq.Cursor cur = p.cursor(); try {
while (cur.next()) { PairSeq.Cursor cur = p.cursor();
String k = cur.key(); while (cur.next()) {
String v = cur.value(); String k = cur.key();
switch (k) { String v = cur.value();
case "mac" -> mac = v; switch (k) {
case "k.b64" -> key = Base64.getDecoder().decode(v); case "mac" -> mac = v;
case "k.hex" -> key = HexFormat.of().parseHex(v); case "k.b64" -> {
default -> { wipe(key);
key = Base64.getDecoder().decode(v);
}
case "k.hex" -> {
wipe(key);
key = HexFormat.of().parseHex(v);
}
default -> {
}
} }
} }
if (mac == null) {
throw new IllegalArgumentException("mac missing for HMAC key");
}
if (key == null) {
throw new IllegalArgumentException("HMAC key missing (k.b64 or k.hex)");
}
return new HmacKeyImportSpec(mac, key);
} finally {
wipe(key);
} }
if (mac == null) { }
throw new IllegalArgumentException("mac missing for HMAC key");
private static void wipe(byte[] current) {
if (current != null) {
Arrays.fill(current, (byte) 0);
} }
if (key == null) { }
throw new IllegalArgumentException("HMAC key missing (k.b64 or k.hex)");
private String encodedKey() {
lifecycleLock.lock();
try {
ensureActive();
return Base64.getEncoder().withoutPadding().encodeToString(key);
} finally {
lifecycleLock.unlock();
}
}
/** {@inheritDoc} */
@Override
public void destroy() {
lifecycleLock.lock();
try {
if (!destroyed) {
Arrays.fill(key, (byte) 0);
destroyed = true;
}
} finally {
lifecycleLock.unlock();
}
}
/** {@inheritDoc} */
@Override
public boolean isDestroyed() {
lifecycleLock.lock();
try {
return destroyed;
} finally {
lifecycleLock.unlock();
}
}
private void ensureActive() {
if (destroyed) {
throw new IllegalStateException("HMAC key import specification has been destroyed");
} }
return new HmacKeyImportSpec(mac, key);
} }
} }

View File

@@ -100,7 +100,7 @@ import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate;
* <h2>Usage example</h2> * <h2>Usage example</h2>
* <h3>Produce HMAC-SHA256 trailer</h3> <pre> * <h3>Produce HMAC-SHA256 trailer</h3> <pre>
* {@code * {@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"); * HmacMacContext ctx = new HmacMacContext(CryptoAlgorithms.require("HMAC"), key, "HmacSHA256");
* try (InputStream in = ctx.wrap(new FileInputStream("data.bin"))) { * try (InputStream in = ctx.wrap(new FileInputStream("data.bin"))) {
* in.transferTo(OutputStream.nullOutputStream()); // body then MAC trailer * in.transferTo(OutputStream.nullOutputStream()); // body then MAC trailer
@@ -135,7 +135,7 @@ public final class HmacMacContext implements MacContext {
// lifecycle // lifecycle
private boolean wrapped; // = false; private boolean wrapped; // = false;
private Stream activeStream; private HmacStream activeStream;
private boolean autoCloseActiveStream; private boolean autoCloseActiveStream;
/** /**
@@ -238,7 +238,7 @@ public final class HmacMacContext implements MacContext {
throw new IOException("HMAC init failed for " + macName, e); 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; this.activeStream = s;
return s; return s;
} }

View File

@@ -54,10 +54,10 @@ import zeroecho.core.spec.ContextSpec;
* *
* <h2>Usage</h2> This spec is passed when creating a new HMAC context: * <h2>Usage</h2> This spec is passed when creating a new HMAC context:
* <pre>{@code * <pre>{@code
* SecretKey key = CryptoAlgorithms.generateSecret("HMAC", * SecretKey key = session.keyBuilders().symmetric().generate("HMAC",
* HmacKeyGenSpec.sha256(256)); * HmacKeyGenSpec.sha256(256));
* *
* MacContext ctx = CryptoAlgorithms.create("HMAC", * MacContext ctx = session.createContext("HMAC",
* KeyUsage.MAC, key, HmacSpec.sha256()); * KeyUsage.MAC, key, HmacSpec.sha256());
* *
* ctx.update(data); * ctx.update(data);

View File

@@ -81,7 +81,7 @@ import zeroecho.core.util.Strings;
* Mac mac = Mac.getInstance("HmacSHA256"); * Mac mac = Mac.getInstance("HmacSHA256");
* mac.init(secretKey); * mac.init(secretKey);
* try (InputStream in = Files.newInputStream(path); * try (InputStream in = Files.newInputStream(path);
* InputStream s = new Stream(in, mac, "HmacSHA256", null, * InputStream s = new HmacStream(in, mac, "HmacSHA256", null,
* new ByteVerificationStrategy())) { * new ByteVerificationStrategy())) {
* // read from 's' to consume body; trailer is produced automatically * // read from 's' to consume body; trailer is produced automatically
* } * }
@@ -98,14 +98,14 @@ import zeroecho.core.util.Strings;
* new ByteVerificationStrategy().getThrowOnMismatch(); * new ByteVerificationStrategy().getThrowOnMismatch();
* *
* try (InputStream in = Files.newInputStream(path); * 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 * // read from 's'; exception is thrown at EOF if verification fails
* } * }
* } * }
* </pre> * </pre>
*/ */
final class Stream extends AbstractPassthroughInputStream { final class HmacStream extends AbstractPassthroughInputStream {
private static final Logger LOG = Logger.getLogger(Stream.class.getName()); private static final Logger LOG = Logger.getLogger(HmacStream.class.getName());
private final Mac mac; private final Mac mac;
private final String macName; private final String macName;
@@ -135,7 +135,7 @@ final class Stream extends AbstractPassthroughInputStream {
* @throws NullPointerException if {@code upstream}, {@code mac}, or * @throws NullPointerException if {@code upstream}, {@code mac}, or
* {@code macName} is {@code null} * {@code macName} is {@code null}
*/ */
/* package */ Stream(final InputStream upstream, final Mac mac, final String macName, final byte[] expectedTag, /* package */ HmacStream(final InputStream upstream, final Mac mac, final String macName, final byte[] expectedTag,
final VerificationBiPredicate<byte[]> verificationStrategy) { final VerificationBiPredicate<byte[]> verificationStrategy) {
super(upstream, 8192); super(upstream, 8192);
this.mac = mac; this.mac = mac;

View File

@@ -48,8 +48,8 @@
* <li>Expose a streaming {@link zeroecho.core.context.MacContext} that appends * <li>Expose a streaming {@link zeroecho.core.context.MacContext} that appends
* tags in produce mode or verifies an expected tag at end of stream in verify * tags in produce mode or verifies an expected tag at end of stream in verify
* mode.</li> * mode.</li>
* <li>Provide immutable specs for selecting the HMAC variant and for supplying * <li>Provide immutable specs for selecting the HMAC variant and destroyable
* keys (generation or import of raw key material).</li> * specs for importing raw key material.</li>
* <li>Encapsulate JCA/JCE interop and provider checks behind small * <li>Encapsulate JCA/JCE interop and provider checks behind small
* factories.</li> * factories.</li>
* </ul> * </ul>
@@ -68,7 +68,7 @@
* specific HMAC variant.</li> * specific HMAC variant.</li>
* <li><b>HmacKeyImportSpec</b>: wrapper for importing existing raw keys, with * <li><b>HmacKeyImportSpec</b>: wrapper for importing existing raw keys, with
* Base64/hex helpers.</li> * Base64/hex helpers.</li>
* <li><b>Stream</b>: internal passthrough input stream implementing the * <li><b>HmacStream</b>: internal passthrough input stream implementing the
* byte-pumping and trailer/verification logic for the MAC context.</li> * byte-pumping and trailer/verification logic for the MAC context.</li>
* </ul> * </ul>
* *

View File

@@ -44,6 +44,7 @@ import java.security.PublicKey;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; 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.KemContext;
import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.context.MessageAgreementContext;
import zeroecho.core.spec.VoidSpec; import zeroecho.core.spec.VoidSpec;
import zeroecho.core.spi.AsymmetricKeyBuilder; import zeroecho.core.spi.AsymmetricKeyPairGenerator;
import zeroecho.core.spi.PrivateKeyImporter;
import zeroecho.core.spi.PublicKeyImporter;
/** /**
* <h2>HQC (Hamming Quasi-Cyclic) Algorithm Integration</h2> * <h2>HQC (Hamming Quasi-Cyclic) Algorithm Integration</h2>
@@ -124,13 +127,14 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* <h2>Usage example</h2> <pre>{@code * <h2>Usage example</h2> <pre>{@code
* // Generate an HQC key pair * // Generate an HQC key pair
* HqcAlgorithm hqc = new HqcAlgorithm(); * HqcAlgorithm hqc = new HqcAlgorithm();
* KeyPair kp = hqc.generateKeyPair(HqcKeyGenSpec.hqc256()); * KeyPair kp = hqc.asymmetricKeyPairGenerator(HqcKeyGenSpec.class)
* .generateKeyPair(HqcKeyGenSpec.hqc256());
* *
* // Encapsulation by initiator * // 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 * // Decapsulation by responder
* KemContext decapsCtx = hqc.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE); * KemContext decapsCtx = hqc.createContext(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
* }</pre> * }</pre>
* *
* @since 1.0 * @since 1.0
@@ -200,7 +204,7 @@ public final class HqcAlgorithm extends AbstractCryptoAlgorithm {
.build(); .build();
}, () -> VoidSpec.INSTANCE); }, () -> VoidSpec.INSTANCE);
registerAsymmetricKeyBuilder(HqcKeyGenSpec.class, new AsymmetricKeyBuilder<>() { registerAsymmetricKeyPairGenerator(HqcKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
@Override @Override
public KeyPair generateKeyPair(HqcKeyGenSpec spec) throws GeneralSecurityException { public KeyPair generateKeyPair(HqcKeyGenSpec spec) throws GeneralSecurityException {
ensureProvider(); ensureProvider();
@@ -213,23 +217,9 @@ public final class HqcAlgorithm extends AbstractCryptoAlgorithm {
kpg.initialize(params, new SecureRandom()); kpg.initialize(params, new SecureRandom());
return kpg.generateKeyPair(); return kpg.generateKeyPair();
} }
@Override
public PublicKey importPublic(HqcKeyGenSpec spec) {
throw new UnsupportedOperationException();
}
@Override
public PrivateKey importPrivate(HqcKeyGenSpec spec) {
throw new UnsupportedOperationException();
}
}, HqcKeyGenSpec::hqc256); }, HqcKeyGenSpec::hqc256);
registerAsymmetricKeyBuilder(HqcPublicKeySpec.class, new AsymmetricKeyBuilder<>() { registerPublicKeyImporter(HqcPublicKeySpec.class, new PublicKeyImporter<>() {
@Override
public KeyPair generateKeyPair(HqcPublicKeySpec spec) {
throw new UnsupportedOperationException();
}
@Override @Override
public PublicKey importPublic(HqcPublicKeySpec spec) throws GeneralSecurityException { public PublicKey importPublic(HqcPublicKeySpec spec) throws GeneralSecurityException {
@@ -237,31 +227,22 @@ public final class HqcAlgorithm extends AbstractCryptoAlgorithm {
KeyFactory kf = KeyFactory.getInstance("HQC", providerName()); KeyFactory kf = KeyFactory.getInstance("HQC", providerName());
return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
} }
});
@Override registerPrivateKeyImporter(HqcPrivateKeySpec.class, new PrivateKeyImporter<>() {
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();
}
@Override @Override
public PrivateKey importPrivate(HqcPrivateKeySpec spec) throws GeneralSecurityException { public PrivateKey importPrivate(HqcPrivateKeySpec spec) throws GeneralSecurityException {
ensureProvider(); ensureProvider();
KeyFactory kf = KeyFactory.getInstance("HQC", providerName()); 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 { private static void ensureProvider() throws NoSuchProviderException {

View File

@@ -65,7 +65,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* // Generate a key pair via HqcAlgorithm * // Generate a key pair via HqcAlgorithm
* HqcAlgorithm hqc = new HqcAlgorithm(); * HqcAlgorithm hqc = new HqcAlgorithm();
* KeyPair kp = hqc.generateKeyPair(spec); * KeyPair kp = hqc.asymmetricKeyPairGenerator(HqcKeyGenSpec.class).generateKeyPair(spec);
* }</pre> * }</pre>
* *
* @since 1.0 * @since 1.0

View File

@@ -33,8 +33,12 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.core.alg.hqc; package zeroecho.core.alg.hqc;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Objects; 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;
import zeroecho.core.marshal.PairSeq.Cursor; import zeroecho.core.marshal.PairSeq.Cursor;
@@ -48,8 +52,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* <p> * <p>
* This class is used to transport and import HQC private keys into the * This class is used to transport and import HQC private keys into the
* {@link HqcAlgorithm}. The encoded form is immutable and defensively copied on * {@link HqcAlgorithm}. The encoded form is defensively copied on construction
* construction and retrieval. * and retrieval and may be destroyed.
* </p> * </p>
* *
* <h2>Serialization</h2> * <h2>Serialization</h2>
@@ -73,7 +77,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* // Import into a PrivateKey via HqcAlgorithm * // Import into a PrivateKey via HqcAlgorithm
* HqcAlgorithm hqc = new HqcAlgorithm(); * HqcAlgorithm hqc = new HqcAlgorithm();
* PrivateKey priv = hqc.importPrivate(spec); * PrivateKey priv = hqc.privateKeyImporter(HqcPrivateKeySpec.class).importPrivate(spec);
* *
* // Serialize for transport * // Serialize for transport
* PairSeq serialized = HqcPrivateKeySpec.marshal(spec); * PairSeq serialized = HqcPrivateKeySpec.marshal(spec);
@@ -84,10 +88,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* @since 1.0 * @since 1.0
*/ */
public final class HqcPrivateKeySpec implements AlgorithmKeySpec { public final class HqcPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
private static final String PKCS8_B64 = "pkcs8.b64"; private static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] pkcs8; private final byte[] pkcs8;
private final ReentrantLock lifecycleLock = new ReentrantLock();
private boolean destroyed;
/** /**
* Constructs a new private key spec from a PKCS#8-encoded byte array. * Constructs a new private key spec from a PKCS#8-encoded byte array.
@@ -105,7 +111,13 @@ public final class HqcPrivateKeySpec implements AlgorithmKeySpec {
* @return cloned PKCS#8 byte array * @return cloned PKCS#8 byte array
*/ */
public byte[] pkcs8() { public byte[] pkcs8() {
return pkcs8.clone(); lifecycleLock.lock();
try {
ensureActive();
return pkcs8.clone();
} finally {
lifecycleLock.unlock();
}
} }
/** /**
@@ -122,7 +134,7 @@ public final class HqcPrivateKeySpec implements AlgorithmKeySpec {
* @return serialized representation in a {@link PairSeq} * @return serialized representation in a {@link PairSeq}
*/ */
public static PairSeq marshal(HqcPrivateKeySpec spec) { public static PairSeq marshal(HqcPrivateKeySpec spec) {
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); String b64 = spec.encodedKey();
return PairSeq.of("type", "HqcPrivateKeySpec", PKCS8_B64, b64); return PairSeq.of("type", "HqcPrivateKeySpec", PKCS8_B64, b64);
} }
@@ -144,7 +156,12 @@ public final class HqcPrivateKeySpec implements AlgorithmKeySpec {
if (b64 == null) { if (b64 == null) {
throw new IllegalArgumentException("HqcPrivateKeySpec: missing pkcs8.b64"); throw new IllegalArgumentException("HqcPrivateKeySpec: missing pkcs8.b64");
} }
return new HqcPrivateKeySpec(Base64.getDecoder().decode(b64)); byte[] decoded = Base64.getDecoder().decode(b64);
try {
return new HqcPrivateKeySpec(decoded);
} finally {
Arrays.fill(decoded, (byte) 0);
}
} }
/** /**
@@ -160,4 +177,43 @@ public final class HqcPrivateKeySpec implements AlgorithmKeySpec {
public String toString() { public String toString() {
return "HqcPrivateKeySpec[len=" + pkcs8.length + "]"; return "HqcPrivateKeySpec[len=" + pkcs8.length + "]";
} }
private String encodedKey() {
lifecycleLock.lock();
try {
ensureActive();
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
} finally {
lifecycleLock.unlock();
}
}
@Override
public void destroy() {
lifecycleLock.lock();
try {
if (!destroyed) {
Arrays.fill(pkcs8, (byte) 0);
destroyed = true;
}
} finally {
lifecycleLock.unlock();
}
}
@Override
public boolean isDestroyed() {
lifecycleLock.lock();
try {
return destroyed;
} finally {
lifecycleLock.unlock();
}
}
private void ensureActive() {
if (destroyed) {
throw new IllegalStateException("HQC private key specification has been destroyed");
}
}
} }

View File

@@ -73,7 +73,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* // Import into a PublicKey via HqcAlgorithm * // Import into a PublicKey via HqcAlgorithm
* HqcAlgorithm hqc = new HqcAlgorithm(); * HqcAlgorithm hqc = new HqcAlgorithm();
* PublicKey pub = hqc.importPublic(spec); * PublicKey pub = hqc.publicKeyImporter(HqcPublicKeySpec.class).importPublic(spec);
* *
* // Serialize for transport * // Serialize for transport
* PairSeq serialized = HqcPublicKeySpec.marshal(spec); * PairSeq serialized = HqcPublicKeySpec.marshal(spec);

View File

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

View File

@@ -44,6 +44,7 @@ import java.security.PublicKey;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
import java.security.spec.X509EncodedKeySpec; import java.security.spec.X509EncodedKeySpec;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; 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.KemContext;
import zeroecho.core.context.MessageAgreementContext; import zeroecho.core.context.MessageAgreementContext;
import zeroecho.core.spec.VoidSpec; import zeroecho.core.spec.VoidSpec;
import zeroecho.core.spi.AsymmetricKeyBuilder; import zeroecho.core.spi.AsymmetricKeyPairGenerator;
import zeroecho.core.spi.PrivateKeyImporter;
import zeroecho.core.spi.PublicKeyImporter;
/** /**
* Concrete CryptoAlgorithm implementation for the post-quantum key * Concrete CryptoAlgorithm implementation for the post-quantum key
@@ -112,22 +115,22 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
* KyberAlgorithm kyber = new KyberAlgorithm(); * KyberAlgorithm kyber = new KyberAlgorithm();
* *
* // Generate a Kyber-768 key pair: * // Generate a Kyber-768 key pair:
* KeyPair kp = kyber.asymmetricKeyBuilder(KyberKeyGenSpec.class) * KeyPair kp = kyber.asymmetricKeyPairGenerator(KyberKeyGenSpec.class)
* .generateKeyPair(KyberKeyGenSpec.kyber768()); * .generateKeyPair(KyberKeyGenSpec.kyber768());
* *
* // Encapsulation by initiator (recipient public key known): * // Encapsulation by initiator (recipient public key known):
* KemContext enc = kyber.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE); * KemContext enc = kyber.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
* *
* // Decapsulation by responder (own private key): * // Decapsulation by responder (own private key):
* KemContext dec = kyber.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE); * KemContext dec = kyber.createContext(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
* *
* // Message-style agreement (initiator): * // Message-style agreement (initiator):
* MessageAgreementContext initCtx = * MessageAgreementContext initCtx =
* kyber.create(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE); * kyber.createContext(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE);
* *
* // Message-style agreement (responder): * // Message-style agreement (responder):
* MessageAgreementContext respCtx = * MessageAgreementContext respCtx =
* kyber.create(KeyUsage.AGREEMENT, kp.getPrivate(), VoidSpec.INSTANCE); * kyber.createContext(KeyUsage.AGREEMENT, kp.getPrivate(), VoidSpec.INSTANCE);
* }</pre> * }</pre>
*/ */
public final class KyberAlgorithm extends AbstractCryptoAlgorithm { public final class KyberAlgorithm extends AbstractCryptoAlgorithm {
@@ -154,7 +157,7 @@ public final class KyberAlgorithm extends AbstractCryptoAlgorithm {
* Security.addProvider(new BouncyCastlePQCProvider()); * Security.addProvider(new BouncyCastlePQCProvider());
* KyberAlgorithm alg = new KyberAlgorithm(); * KyberAlgorithm alg = new KyberAlgorithm();
* *
* KeyPair kp = alg.asymmetricKeyBuilder(KyberKeyGenSpec.class) * KeyPair kp = alg.asymmetricKeyPairGenerator(KyberKeyGenSpec.class)
* .generateKeyPair(KyberKeyGenSpec.kyber768()); * .generateKeyPair(KyberKeyGenSpec.kyber768());
* }</pre> * }</pre>
*/ */
@@ -189,7 +192,7 @@ public final class KyberAlgorithm extends AbstractCryptoAlgorithm {
}, () -> VoidSpec.INSTANCE); }, () -> VoidSpec.INSTANCE);
// Keypair builder via BCPQC // Keypair builder via BCPQC
registerAsymmetricKeyBuilder(KyberKeyGenSpec.class, new AsymmetricKeyBuilder<>() { registerAsymmetricKeyPairGenerator(KyberKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
/** /**
* Generates a Kyber key pair for the variant defined by the provided spec. * Generates a Kyber key pair for the variant defined by the provided spec.
* *
@@ -206,53 +209,10 @@ public final class KyberAlgorithm extends AbstractCryptoAlgorithm {
kpg.initialize(params, new SecureRandom()); kpg.initialize(params, new SecureRandom());
return kpg.generateKeyPair(); return kpg.generateKeyPair();
} }
/**
* Unsupported operation for this builder. Use {@code KyberPublicKeySpec} for
* public key import.
*
* @param spec the key generation spec; not used.
* @return never returns normally.
* @throws UnsupportedOperationException always thrown to indicate that public
* key import uses a dedicated encoded
* spec.
*/
@Override
public PublicKey importPublic(KyberKeyGenSpec spec) {
throw new UnsupportedOperationException("Import with a dedicated encoded spec, if needed.");
}
/**
* Unsupported operation for this builder. Use {@code KyberPrivateKeySpec} for
* private key import.
*
* @param spec the key generation spec; not used.
* @return never returns normally.
* @throws UnsupportedOperationException always thrown to indicate that private
* key import uses a dedicated encoded
* spec.
*/
@Override
public PrivateKey importPrivate(KyberKeyGenSpec spec) {
throw new UnsupportedOperationException("Import with a dedicated encoded spec, if needed.");
}
}, KyberKeyGenSpec::kyber768); }, KyberKeyGenSpec::kyber768);
// Public-key import (X.509) // Public-key import (X.509)
registerAsymmetricKeyBuilder(KyberPublicKeySpec.class, new AsymmetricKeyBuilder<>() { registerPublicKeyImporter(KyberPublicKeySpec.class, new PublicKeyImporter<>() {
/**
* Unsupported operation for this builder. Use {@code KyberKeyGenSpec} for
* generation.
*
* @param spec the public key spec; not used.
* @return never returns normally.
* @throws UnsupportedOperationException always thrown to indicate that key
* generation is not supported here.
*/
@Override
public KeyPair generateKeyPair(KyberPublicKeySpec spec) {
throw new UnsupportedOperationException("Use KyberKeyGenSpec for generation");
}
/** /**
* Imports a Kyber public key from an X.509 SubjectPublicKeyInfo encoding. * Imports a Kyber public key from an X.509 SubjectPublicKeyInfo encoding.
@@ -269,52 +229,10 @@ public final class KyberAlgorithm extends AbstractCryptoAlgorithm {
KeyFactory kf = KeyFactory.getInstance("Kyber", providerName()); KeyFactory kf = KeyFactory.getInstance("Kyber", providerName());
return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
} }
});
/**
* Unsupported operation for this builder. Use {@code KyberPrivateKeySpec} for
* private key import.
*
* @param spec the public key spec; not used.
* @return never returns normally.
* @throws UnsupportedOperationException always thrown to indicate that private
* key import is not supported here.
*/
@Override
public PrivateKey importPrivate(KyberPublicKeySpec spec) {
throw new UnsupportedOperationException("Use KyberPrivateKeySpec for private key import");
}
}, null // no default spec
);
// Private-key import (PKCS#8) // Private-key import (PKCS#8)
registerAsymmetricKeyBuilder(KyberPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { registerPrivateKeyImporter(KyberPrivateKeySpec.class, new PrivateKeyImporter<>() {
/**
* Unsupported operation for this builder. Use {@code KyberKeyGenSpec} for
* generation.
*
* @param spec the private key spec; not used.
* @return never returns normally.
* @throws UnsupportedOperationException always thrown to indicate that key
* generation is not supported here.
*/
@Override
public KeyPair generateKeyPair(KyberPrivateKeySpec spec) {
throw new UnsupportedOperationException("Use KyberKeyGenSpec for generation");
}
/**
* Unsupported operation for this builder. Use {@code KyberPublicKeySpec} for
* public key import.
*
* @param spec the private key spec; not used.
* @return never returns normally.
* @throws UnsupportedOperationException always thrown to indicate that public
* key import is not supported here.
*/
@Override
public PublicKey importPublic(KyberPrivateKeySpec spec) {
throw new UnsupportedOperationException("Use KyberPublicKeySpec for public key import");
}
/** /**
* Imports a Kyber private key from a PKCS#8 PrivateKeyInfo encoding. * Imports a Kyber private key from a PKCS#8 PrivateKeyInfo encoding.
@@ -328,9 +246,14 @@ public final class KyberAlgorithm extends AbstractCryptoAlgorithm {
public PrivateKey importPrivate(KyberPrivateKeySpec spec) throws GeneralSecurityException { public PrivateKey importPrivate(KyberPrivateKeySpec spec) throws GeneralSecurityException {
ensureProvider(); ensureProvider();
KeyFactory kf = KeyFactory.getInstance("Kyber", providerName()); KeyFactory kf = KeyFactory.getInstance("Kyber", providerName());
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); byte[] encoded = spec.pkcs8();
try {
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
} finally {
Arrays.fill(encoded, (byte) 0);
}
} }
}, null); });
} }
/** /**

View File

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

View File

@@ -33,8 +33,12 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.core.alg.kyber; package zeroecho.core.alg.kyber;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Objects; 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;
import zeroecho.core.spec.AlgorithmKeySpec; 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. * Specification wrapper for a Kyber (ML-KEM) private key encoded in PKCS#8.
* *
* <p> * <p>
* Instances of this class carry an immutable copy of the PKCS#8-encoded private * Instances of this class carry an owned copy of the PKCS#8-encoded private
* key bytes. They are used with {@link zeroecho.core.CryptoAlgorithm} key * key bytes. They are used with {@link zeroecho.core.CryptoAlgorithm} key
* builders to import keys into the providers native representation. * builders to import keys into the providers native representation.
* </p> * </p>
@@ -51,7 +55,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <h2>Encoding</h2> * <h2>Encoding</h2>
* <ul> * <ul>
* <li>Format: PKCS#8 DER encoding of a Kyber private key.</li> * <li>Format: PKCS#8 DER encoding of a Kyber private key.</li>
* <li>Stored as a defensive clone to ensure immutability.</li> * <li>Stored as a defensive clone.</li>
* <li>Marshalling/unmarshalling supported via {@link PairSeq} with Base64 * <li>Marshalling/unmarshalling supported via {@link PairSeq} with Base64
* encoding.</li> * encoding.</li>
* </ul> * </ul>
@@ -60,7 +64,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* // Import a private key into a CryptoAlgorithm * // Import a private key into a CryptoAlgorithm
* byte[] pkcs8Bytes = ...; // obtained from storage * byte[] pkcs8Bytes = ...; // obtained from storage
* KyberPrivateKeySpec spec = new KyberPrivateKeySpec(pkcs8Bytes); * KyberPrivateKeySpec spec = new KyberPrivateKeySpec(pkcs8Bytes);
* PrivateKey k = kyberAlg.importPrivate(spec); * PrivateKey k = kyberAlg.privateKeyImporter(KyberPrivateKeySpec.class).importPrivate(spec);
* *
* // Serialize for persistence * // Serialize for persistence
* PairSeq seq = KyberPrivateKeySpec.marshal(spec); * PairSeq seq = KyberPrivateKeySpec.marshal(spec);
@@ -70,16 +74,18 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* }</pre> * }</pre>
* *
* <p> * <p>
* This class is immutable and thread-safe. * Access and destruction are synchronized.
* </p> * </p>
* *
* @see KyberPublicKeySpec * @see KyberPublicKeySpec
* @see KyberKeyGenSpec * @see KyberKeyGenSpec
*/ */
public final class KyberPrivateKeySpec implements AlgorithmKeySpec { public final class KyberPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
private static final String PKCS8_B64 = "pkcs8.b64"; private static final String PKCS8_B64 = "pkcs8.b64";
private final byte[] pkcs8; private final byte[] pkcs8;
private final ReentrantLock lifecycleLock = new ReentrantLock();
private boolean destroyed;
/** /**
* Creates a new spec from PKCS#8-encoded private key bytes. * 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 * @return cloned byte array of PKCS#8 DER encoding
*/ */
public byte[] pkcs8() { 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 * @return key-value representation suitable for persistence
*/ */
public static PairSeq marshal(KyberPrivateKeySpec spec) { 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); return PairSeq.of("type", "KyberPrivateKey", PKCS8_B64, b64);
} }
@@ -139,7 +151,12 @@ public final class KyberPrivateKeySpec implements AlgorithmKeySpec {
if (pkcs8b64 == null) { if (pkcs8b64 == null) {
throw new IllegalArgumentException("KyberPrivateKeySpec: missing 'pkcs8.b64'"); 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() { public String toString() {
return "KyberPrivateKeySpec[len=" + pkcs8.length + "]"; return "KyberPrivateKeySpec[len=" + pkcs8.length + "]";
} }
private String encodedKey() {
lifecycleLock.lock();
try {
ensureActive();
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
} finally {
lifecycleLock.unlock();
}
}
@Override
public void destroy() {
lifecycleLock.lock();
try {
if (!destroyed) {
Arrays.fill(pkcs8, (byte) 0);
destroyed = true;
}
} finally {
lifecycleLock.unlock();
}
}
@Override
public boolean isDestroyed() {
lifecycleLock.lock();
try {
return destroyed;
} finally {
lifecycleLock.unlock();
}
}
private void ensureActive() {
if (destroyed) {
throw new IllegalStateException("Kyber private key specification has been destroyed");
}
}
} }

View File

@@ -62,7 +62,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* // Import a public key into a CryptoAlgorithm * // Import a public key into a CryptoAlgorithm
* byte[] x509Bytes = ...; // obtained from storage * byte[] x509Bytes = ...; // obtained from storage
* KyberPublicKeySpec spec = new KyberPublicKeySpec(x509Bytes); * KyberPublicKeySpec spec = new KyberPublicKeySpec(x509Bytes);
* PublicKey k = kyberAlg.importPublic(spec); * PublicKey k = kyberAlg.publicKeyImporter(KyberPublicKeySpec.class).importPublic(spec);
* *
* // Serialize for persistence * // Serialize for persistence
* PairSeq seq = KyberPublicKeySpec.marshal(spec); * PairSeq seq = KyberPublicKeySpec.marshal(spec);

View File

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

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