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:
@@ -40,7 +40,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -52,17 +52,21 @@ import org.apache.commons.cli.OptionGroup;
|
||||
import org.apache.commons.cli.Options;
|
||||
import org.apache.commons.cli.ParseException;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.context.EncryptionContext;
|
||||
import zeroecho.core.context.KemContext;
|
||||
import zeroecho.core.err.UnsupportedRoleException;
|
||||
import zeroecho.core.storage.KeyringStore;
|
||||
import zeroecho.sdk.Pbkdf2Limits;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder;
|
||||
import zeroecho.sdk.builders.core.PlainFileBuilder;
|
||||
import zeroecho.sdk.content.api.DataContent;
|
||||
import zeroecho.sdk.guard.MultiRecipientContent;
|
||||
import zeroecho.sdk.guard.MultiRecipientDataSourceBuilder;
|
||||
import zeroecho.sdk.guard.RecipientKekSizes;
|
||||
import zeroecho.sdk.guard.UnlockMaterial;
|
||||
import zeroecho.sdk.util.RandomSupport;
|
||||
|
||||
/**
|
||||
* Guard is a unified subcommand that encrypts and decrypts using a
|
||||
@@ -97,6 +101,8 @@ import zeroecho.sdk.guard.UnlockMaterial;
|
||||
* ZeroEcho -G --encrypt in.bin --ks keys.txt \
|
||||
* --to-alias alice --to-alias bob \
|
||||
* --decoy-psw-rand 2 \
|
||||
* --pbkdf2-max "$PBKDF2_POLICY_MAX" \
|
||||
* --pbkdf2-hard-max "$PBKDF2_HARD_MAX" \
|
||||
* --alg aes-gcm --tag-bits 128 --aad-hex 01ff
|
||||
*
|
||||
* # Decrypt with private key (payload is ChaCha20-Poly1305)
|
||||
@@ -124,7 +130,6 @@ public final class Guard {
|
||||
*/
|
||||
public static int main(final String[] args, final Options options) // NOPMD
|
||||
throws ParseException, IOException, GeneralSecurityException {
|
||||
|
||||
// ---- operation selection
|
||||
final Option OPT_ENCRYPT = Option.builder("e").longOpt("encrypt").hasArg().argName("in-file")
|
||||
.desc("Encrypt the given file").get();
|
||||
@@ -177,7 +182,12 @@ public final class Guard {
|
||||
final Option OPT_PSW_SALT = Option.builder().longOpt("to-salt-len").hasArg().argName("bytes")
|
||||
.desc("PBKDF2 salt length for password recipients (default 16)").get();
|
||||
final Option OPT_PSW_KEK = Option.builder().longOpt("to-kek-bytes").hasArg().argName("bytes")
|
||||
.desc("Derived KEK length for password recipients (default 32)").get();
|
||||
.desc("Recipient KEK length: exactly 16 or 32 bytes (default 32)").get();
|
||||
final Option OPT_PBKDF2_MAX = Option.builder().longOpt("pbkdf2-max").hasArg().argName("iterations")
|
||||
.desc("Operational PBKDF2 ceiling; required for password operations").get();
|
||||
final Option OPT_PBKDF2_HARD_MAX = Option.builder().longOpt("pbkdf2-hard-max").hasArg()
|
||||
.argName("iterations")
|
||||
.desc("Absolute decoded PBKDF2 safety ceiling; required for password operations").get();
|
||||
|
||||
// ---- decoys (all types)
|
||||
final Option OPT_DECOY_ALIAS = Option.builder().longOpt("decoy-alias").hasArg().argName("alias")
|
||||
@@ -215,6 +225,8 @@ public final class Guard {
|
||||
options.addOption(OPT_PSW_ITER);
|
||||
options.addOption(OPT_PSW_SALT);
|
||||
options.addOption(OPT_PSW_KEK);
|
||||
options.addOption(OPT_PBKDF2_MAX);
|
||||
options.addOption(OPT_PBKDF2_HARD_MAX);
|
||||
|
||||
options.addOption(OPT_DECOY_ALIAS);
|
||||
options.addOption(OPT_DECOY_PSW);
|
||||
@@ -225,6 +237,10 @@ public final class Guard {
|
||||
|
||||
final CommandLineParser parser = new DefaultParser();
|
||||
final CommandLine cmd = parser.parse(options, args);
|
||||
final boolean passwordOperation = cmd.hasOption(OPT_TO_PSW) || cmd.hasOption(OPT_DECOY_PSW)
|
||||
|| cmd.hasOption(OPT_DECOY_PSW_RAND) || cmd.hasOption(OPT_PASSWORD);
|
||||
final ZeroEchoSession session = createSession(cmd, OPT_PBKDF2_MAX, OPT_PBKDF2_HARD_MAX,
|
||||
passwordOperation);
|
||||
|
||||
final boolean encrypt = cmd.hasOption(OPT_ENCRYPT);
|
||||
final Path inPath = Paths.get(cmd.getOptionValue(encrypt ? OPT_ENCRYPT : OPT_DECRYPT));
|
||||
@@ -249,7 +265,7 @@ public final class Guard {
|
||||
final ChaChaDataContentBuilder chacha;
|
||||
switch (alg) {
|
||||
case "aes-gcm" -> {
|
||||
aes = AesDataContentBuilder.builder().modeGcm(tagBits);
|
||||
aes = AesDataContentBuilder.builder(session).modeGcm(tagBits);
|
||||
if (header) {
|
||||
aes.withHeader();
|
||||
}
|
||||
@@ -259,28 +275,28 @@ public final class Guard {
|
||||
chacha = null;
|
||||
}
|
||||
case "aes-ctr" -> {
|
||||
aes = AesDataContentBuilder.builder().modeCtr();
|
||||
aes = AesDataContentBuilder.builder(session).modeCtr();
|
||||
if (header) {
|
||||
aes.withHeader();
|
||||
}
|
||||
chacha = null;
|
||||
}
|
||||
case "aes-cbc-pkcs7" -> {
|
||||
aes = AesDataContentBuilder.builder().modeCbcPkcs5();
|
||||
aes = AesDataContentBuilder.builder(session).modeCbcPkcs5();
|
||||
if (header) {
|
||||
aes.withHeader();
|
||||
}
|
||||
chacha = null;
|
||||
}
|
||||
case "aes-cbc-nopad" -> {
|
||||
aes = AesDataContentBuilder.builder().modeCbcNoPadding();
|
||||
aes = AesDataContentBuilder.builder(session).modeCbcNoPadding();
|
||||
if (header) {
|
||||
aes.withHeader();
|
||||
}
|
||||
chacha = null;
|
||||
}
|
||||
case "chacha-aead" -> {
|
||||
chacha = ChaChaDataContentBuilder.builder();
|
||||
chacha = ChaChaDataContentBuilder.builder(session);
|
||||
// selecting AEAD: if the user did not supply AAD, pass empty to pick AEAD
|
||||
chacha.withAad(aad != null ? aad : new byte[0]);
|
||||
if (header) {
|
||||
@@ -302,7 +318,7 @@ public final class Guard {
|
||||
aes = null;
|
||||
}
|
||||
case "chacha-stream" -> {
|
||||
chacha = ChaChaDataContentBuilder.builder();
|
||||
chacha = ChaChaDataContentBuilder.builder(session);
|
||||
if (header) {
|
||||
chacha.withHeader();
|
||||
}
|
||||
@@ -321,71 +337,93 @@ public final class Guard {
|
||||
}
|
||||
|
||||
// envelope builder (new API)
|
||||
final MultiRecipientDataSourceBuilder env = new MultiRecipientDataSourceBuilder().payloadKeyBytes(cekBytes)
|
||||
final MultiRecipientDataSourceBuilder env = MultiRecipientDataSourceBuilder.builder(session)
|
||||
.payloadKeyBytes(cekBytes)
|
||||
.headerLimits(maxRecipients, maxEntryLen);
|
||||
if (aes != null) {
|
||||
env.withAes(aes);
|
||||
} else {
|
||||
env.withChaCha(chacha);
|
||||
}
|
||||
// shuffle on by default
|
||||
if (shuffle) {
|
||||
env.shuffle();
|
||||
}
|
||||
// recipients and decoys only apply on encrypt
|
||||
if (encrypt) {
|
||||
final int iter = Integer.parseInt(cmd.getOptionValue(OPT_PSW_ITER, "200000"));
|
||||
final int saltLen = Integer.parseInt(cmd.getOptionValue(OPT_PSW_SALT, "16"));
|
||||
final int kekLen = Integer.parseInt(cmd.getOptionValue(OPT_PSW_KEK, "32"));
|
||||
|
||||
final KeyringStore ks = loadKeyringIfPresent(cmd, OPT_KEYRING);
|
||||
// real recipients by alias
|
||||
for (String alias : cmd.getOptionValues(OPT_TO_ALIAS) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_TO_ALIAS)) {
|
||||
addRecipientFromAlias(env, ks, alias, kekLen, saltLen, false);
|
||||
}
|
||||
// real password recipients
|
||||
for (String psw : cmd.getOptionValues(OPT_TO_PSW) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_TO_PSW)) {
|
||||
env.addPasswordRecipient(psw.toCharArray(), iter, saltLen, kekLen);
|
||||
}
|
||||
// decoys by alias (key types)
|
||||
for (String alias : cmd.getOptionValues(OPT_DECOY_ALIAS) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_DECOY_ALIAS)) {
|
||||
addRecipientFromAlias(env, ks, alias, kekLen, saltLen, true);
|
||||
}
|
||||
// decoy passwords (explicit)
|
||||
for (String psw : cmd.getOptionValues(OPT_DECOY_PSW) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_DECOY_PSW)) {
|
||||
env.addPasswordRecipientDecoy(psw.toCharArray(), iter, saltLen, kekLen);
|
||||
}
|
||||
// decoy passwords (random)
|
||||
final int rndCount = Integer.parseInt(cmd.getOptionValue(OPT_DECOY_PSW_RAND, "0"));
|
||||
for (int i = 0; i < rndCount; i++) {
|
||||
env.addPasswordRecipientDecoy(randomPassword(), iter, saltLen, kekLen);
|
||||
}
|
||||
} else {
|
||||
// unlock material for decrypt
|
||||
final String privAlias = cmd.getOptionValue(OPT_PRIV_ALIAS);
|
||||
final String password = cmd.getOptionValue(OPT_PASSWORD);
|
||||
|
||||
if ((privAlias == null && password == null) || (privAlias != null && password != null)) {
|
||||
throw new ParseException("Specify exactly one of --priv-alias or --password for decryption");
|
||||
}
|
||||
if (privAlias != null) {
|
||||
final KeyringStore ks = requireKeyring(cmd, OPT_KEYRING);
|
||||
final KeyringStore.PrivateWithId pr = ks.getPrivateWithId(privAlias);
|
||||
env.unlockWith(new UnlockMaterial.Private(pr.key()));
|
||||
UnlockMaterial borrowedUnlockMaterial = null;
|
||||
try (env) {
|
||||
if (aes != null) {
|
||||
env.withAes(aes);
|
||||
} else {
|
||||
env.unlockWith(new UnlockMaterial.Password(password.toCharArray()));
|
||||
env.withChaCha(chacha);
|
||||
}
|
||||
}
|
||||
if (shuffle) {
|
||||
env.shuffle();
|
||||
}
|
||||
if (encrypt) {
|
||||
final int iter = Integer.parseInt(cmd.getOptionValue(OPT_PSW_ITER, "200000"));
|
||||
final int saltLen = Integer.parseInt(cmd.getOptionValue(OPT_PSW_SALT, "16"));
|
||||
final int kekLen = RecipientKekSizes.requireSupported(
|
||||
Integer.parseInt(cmd.getOptionValue(OPT_PSW_KEK, "32")));
|
||||
|
||||
// connect upstream and run
|
||||
final DataContent content = env.build(encrypt); // env installs default openers on decrypt if none were added
|
||||
content.setInput(PlainFileBuilder.builder().url(inPath.toUri().toURL()).build(encrypt));
|
||||
try (InputStream in = content.getStream(); OutputStream out = Files.newOutputStream(outPath)) {
|
||||
in.transferTo(out);
|
||||
final KeyringStore ks = loadKeyringIfPresent(session, cmd, OPT_KEYRING);
|
||||
for (String alias : cmd.getOptionValues(OPT_TO_ALIAS) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_TO_ALIAS)) {
|
||||
addRecipientFromAlias(session, env, ks, alias, kekLen, saltLen, false);
|
||||
}
|
||||
for (String psw : cmd.getOptionValues(OPT_TO_PSW) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_TO_PSW)) {
|
||||
char[] passwordChars = psw.toCharArray();
|
||||
try {
|
||||
env.addPasswordRecipient(passwordChars, iter, saltLen, kekLen);
|
||||
} finally {
|
||||
Arrays.fill(passwordChars, '\0');
|
||||
}
|
||||
}
|
||||
for (String alias : cmd.getOptionValues(OPT_DECOY_ALIAS) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_DECOY_ALIAS)) {
|
||||
addRecipientFromAlias(session, env, ks, alias, kekLen, saltLen, true);
|
||||
}
|
||||
for (String psw : cmd.getOptionValues(OPT_DECOY_PSW) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_DECOY_PSW)) {
|
||||
char[] passwordChars = psw.toCharArray();
|
||||
try {
|
||||
env.addPasswordRecipientDecoy(passwordChars, iter, saltLen, kekLen);
|
||||
} finally {
|
||||
Arrays.fill(passwordChars, '\0');
|
||||
}
|
||||
}
|
||||
final int rndCount = Integer.parseInt(cmd.getOptionValue(OPT_DECOY_PSW_RAND, "0"));
|
||||
for (int i = 0; i < rndCount; i++) {
|
||||
char[] passwordChars = randomPassword();
|
||||
try {
|
||||
env.addPasswordRecipientDecoy(passwordChars, iter, saltLen, kekLen);
|
||||
} finally {
|
||||
Arrays.fill(passwordChars, '\0');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final String privAlias = cmd.getOptionValue(OPT_PRIV_ALIAS);
|
||||
final String password = cmd.getOptionValue(OPT_PASSWORD);
|
||||
|
||||
if ((privAlias == null && password == null) || (privAlias != null && password != null)) {
|
||||
throw new ParseException("Specify exactly one of --priv-alias or --password for decryption");
|
||||
}
|
||||
if (privAlias != null) {
|
||||
final KeyringStore ks = requireKeyring(session, cmd, OPT_KEYRING);
|
||||
final KeyringStore.PrivateWithId pr = ks.getPrivateWithId(privAlias);
|
||||
borrowedUnlockMaterial = new UnlockMaterial.Private(pr.key());
|
||||
} else {
|
||||
char[] passwordChars = password.toCharArray();
|
||||
try {
|
||||
borrowedUnlockMaterial = new UnlockMaterial.Password(passwordChars);
|
||||
} finally {
|
||||
Arrays.fill(passwordChars, '\0');
|
||||
}
|
||||
}
|
||||
env.unlockWith(borrowedUnlockMaterial);
|
||||
}
|
||||
|
||||
try (MultiRecipientContent content = env.build(encrypt)) {
|
||||
content.setInput(PlainFileBuilder.builder().url(inPath.toUri().toURL()).build(encrypt));
|
||||
try (InputStream in = content.getStream(); OutputStream out = Files.newOutputStream(outPath)) {
|
||||
in.transferTo(out);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (borrowedUnlockMaterial instanceof UnlockMaterial.Password passwordMaterial) {
|
||||
passwordMaterial.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -404,6 +442,33 @@ public final class Guard {
|
||||
return Paths.get(s + suffix);
|
||||
}
|
||||
|
||||
private static ZeroEchoSession createSession(CommandLine cmd, Option operationalMaximumOption,
|
||||
Option absoluteMaximumOption, boolean passwordOperation) throws ParseException {
|
||||
boolean hasOperationalMaximum = cmd.hasOption(operationalMaximumOption);
|
||||
boolean hasAbsoluteMaximum = cmd.hasOption(absoluteMaximumOption);
|
||||
if (!hasOperationalMaximum && !hasAbsoluteMaximum) {
|
||||
if (passwordOperation) {
|
||||
throw new ParseException(
|
||||
"Password operations require --pbkdf2-max and --pbkdf2-hard-max");
|
||||
}
|
||||
return new ZeroEchoSession();
|
||||
}
|
||||
if (!hasOperationalMaximum || !hasAbsoluteMaximum) {
|
||||
throw new ParseException("--pbkdf2-max and --pbkdf2-hard-max must be specified together");
|
||||
}
|
||||
try {
|
||||
int operationalMaximum = Integer.parseInt(cmd.getOptionValue(operationalMaximumOption));
|
||||
int absoluteMaximum = Integer.parseInt(cmd.getOptionValue(absoluteMaximumOption));
|
||||
return new ZeroEchoSession().withPbkdf2Limits(
|
||||
new Pbkdf2Limits(operationalMaximum, absoluteMaximum));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
ParseException parseException =
|
||||
new ParseException("Invalid PBKDF2 limits: " + exception.getMessage());
|
||||
parseException.initCause(exception);
|
||||
throw parseException;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] parseHex(String s) throws ParseException {
|
||||
try {
|
||||
return HexFormat.of().parseHex(s);
|
||||
@@ -434,15 +499,14 @@ public final class Guard {
|
||||
*
|
||||
* <p>
|
||||
* In both cases, the created context is consumed by
|
||||
* {@link MultiRecipientDataSourceBuilder#addRecipient(Object)} and is closed
|
||||
* internally by the builder.
|
||||
* the matching {@link MultiRecipientDataSourceBuilder} recipient method and is
|
||||
* closed internally by the resulting content.
|
||||
* </p>
|
||||
*
|
||||
* @param env target builder to which the recipient is added
|
||||
* @param ks keyring store that provides public keys by alias
|
||||
* @param alias alias name of the recipient's public key in the keyring
|
||||
* @param kekBytes desired length in bytes of the key-encryption key when using
|
||||
* KEM
|
||||
* @param kekBytes key-encryption key length; exactly 16 or 32 bytes
|
||||
* @param saltLen salt length in bytes when using KEM
|
||||
* @param decoy whether the recipient is a decoy
|
||||
* @throws GeneralSecurityException if the algorithm does not support the
|
||||
@@ -450,29 +514,52 @@ public final class Guard {
|
||||
* @throws IOException if context creation or builder operations
|
||||
* require I/O and fail
|
||||
*/
|
||||
private static void addRecipientFromAlias(MultiRecipientDataSourceBuilder env, KeyringStore ks, String alias,
|
||||
@SuppressWarnings({ "PMD.CloseResource", "PMD.UseTryWithResources" })
|
||||
private static void addRecipientFromAlias(ZeroEchoSession session, MultiRecipientDataSourceBuilder env,
|
||||
KeyringStore ks, String alias,
|
||||
int kekBytes, int saltLen, boolean decoy) throws GeneralSecurityException, IOException {
|
||||
KeyringStore.PublicWithId r = ks.getPublicWithId(alias);
|
||||
final String algId = r.algorithm();
|
||||
final java.security.PublicKey pub = r.key();
|
||||
|
||||
// Try KEM first
|
||||
try (KemContext kem = CryptoAlgorithms.create(algId, KeyUsage.ENCAPSULATE, pub)) {
|
||||
if (decoy) {
|
||||
env.addRecipientDecoy(kem, kekBytes, saltLen); // builder closes context
|
||||
} else {
|
||||
env.addRecipient(kem, kekBytes, saltLen); // builder closes context
|
||||
}
|
||||
KemContext kem;
|
||||
try {
|
||||
kem = session.createContext(algId, KeyUsage.ENCAPSULATE, pub);
|
||||
} catch (UnsupportedRoleException notKem) {
|
||||
addEncryptionRecipient(session, env, algId, pub, decoy);
|
||||
return;
|
||||
} catch (Exception notKem) { // NOPMD
|
||||
// fall back to public-key encryption
|
||||
}
|
||||
|
||||
try (EncryptionContext enc = CryptoAlgorithms.create(algId, KeyUsage.ENCRYPT, pub)) {
|
||||
boolean transferred = false;
|
||||
try {
|
||||
if (decoy) {
|
||||
env.addRecipientDecoy(enc); // builder closes context
|
||||
env.addRecipientDecoy(kem, kekBytes, saltLen);
|
||||
} else {
|
||||
env.addRecipient(enc); // builder closes context
|
||||
env.addRecipient(kem, kekBytes, saltLen);
|
||||
}
|
||||
transferred = true;
|
||||
} finally {
|
||||
if (!transferred) {
|
||||
kem.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "PMD.CloseResource", "PMD.UseTryWithResources" })
|
||||
private static void addEncryptionRecipient(ZeroEchoSession session, MultiRecipientDataSourceBuilder env,
|
||||
String algorithmId, java.security.PublicKey publicKey, boolean decoy) throws IOException {
|
||||
EncryptionContext encryption = session.createContext(algorithmId, KeyUsage.ENCRYPT, publicKey);
|
||||
boolean transferred = false;
|
||||
try {
|
||||
if (decoy) {
|
||||
env.addRecipientDecoy(encryption);
|
||||
} else {
|
||||
env.addRecipient(encryption);
|
||||
}
|
||||
transferred = true;
|
||||
} finally {
|
||||
if (!transferred) {
|
||||
encryption.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -480,26 +567,27 @@ public final class Guard {
|
||||
private static char[] randomPassword() {
|
||||
// simple random alnum for decoy purposes only
|
||||
final String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
final SecureRandom rnd = new SecureRandom();
|
||||
final int len = 16 + rnd.nextInt(17); // 16..32
|
||||
final int len = 16 + RandomSupport.nextInt(17); // 16..32
|
||||
final char[] out = new char[len];
|
||||
for (int i = 0; i < len; i++) {
|
||||
out[i] = alphabet.charAt(rnd.nextInt(alphabet.length()));
|
||||
out[i] = alphabet.charAt(RandomSupport.nextInt(alphabet.length()));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static KeyringStore loadKeyringIfPresent(CommandLine cmd, Option optKs) throws IOException {
|
||||
private static KeyringStore loadKeyringIfPresent(ZeroEchoSession session, CommandLine cmd, Option optKs)
|
||||
throws IOException {
|
||||
if (!cmd.hasOption(optKs)) {
|
||||
return new KeyringStore();
|
||||
return new KeyringStore(session);
|
||||
}
|
||||
return KeyringStore.load(Paths.get(cmd.getOptionValue(optKs)));
|
||||
return KeyringStore.load(session, Paths.get(cmd.getOptionValue(optKs)));
|
||||
}
|
||||
|
||||
private static KeyringStore requireKeyring(CommandLine cmd, Option optKs) throws IOException, ParseException {
|
||||
private static KeyringStore requireKeyring(ZeroEchoSession session, CommandLine cmd, Option optKs)
|
||||
throws IOException, ParseException {
|
||||
if (!cmd.hasOption(optKs)) {
|
||||
throw new ParseException("--keyring <file> is required when aliases are used");
|
||||
}
|
||||
return KeyringStore.load(Paths.get(cmd.getOptionValue(optKs)));
|
||||
return KeyringStore.load(session, Paths.get(cmd.getOptionValue(optKs)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ import zeroecho.core.storage.KeyringStore;
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder;
|
||||
import zeroecho.sdk.builders.alg.KemDataContentBuilder;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
import zeroecho.sdk.builders.core.DataContentChainBuilder;
|
||||
import zeroecho.sdk.builders.core.PlainFileBuilder;
|
||||
import zeroecho.sdk.content.api.DataContent;
|
||||
@@ -203,6 +204,7 @@ public final class Kem { // NOPMD
|
||||
}
|
||||
|
||||
public static int main(String[] args, Options opts) throws ParseException, IOException, GeneralSecurityException { // NOPMD
|
||||
ZeroEchoSession session = new ZeroEchoSession();
|
||||
defineOptions(opts);
|
||||
CommandLineParser parser = new DefaultParser();
|
||||
CommandLine cmd = parser.parse(opts, args);
|
||||
@@ -240,10 +242,10 @@ public final class Kem { // NOPMD
|
||||
|
||||
final String kemId = cmd.getOptionValue(OPT_KEM.getLongOpt());
|
||||
final Path keyringPath = Path.of(cmd.getOptionValue(OPT_KEYRING.getLongOpt()));
|
||||
final KeyringStore keyring = KeyringStore.load(keyringPath);
|
||||
final KeyringStore keyring = KeyringStore.load(session, keyringPath);
|
||||
|
||||
// Configure KEM envelope
|
||||
KemDataContentBuilder kem = KemDataContentBuilder.builder().kem(kemId);
|
||||
KemDataContentBuilder kem = KemDataContentBuilder.builder(session).kem(kemId);
|
||||
if (cmd.hasOption(OPT_DIRECT.getLongOpt())) {
|
||||
kem = kem.directSecret();
|
||||
} else {
|
||||
@@ -267,7 +269,7 @@ public final class Kem { // NOPMD
|
||||
// AES payload
|
||||
if (wantAes) {
|
||||
String mode = cmd.getOptionValue(OPT_AES_CIPHER.getLongOpt(), "gcm").toLowerCase(Locale.ROOT);
|
||||
AesDataContentBuilder aes = AesDataContentBuilder.builder();
|
||||
AesDataContentBuilder aes = AesDataContentBuilder.builder(session);
|
||||
switch (mode) {
|
||||
case "gcm" -> {
|
||||
Integer tagBitsOpt = parsedIntOpt(cmd, OPT_AES_TAG_BITS);
|
||||
@@ -294,7 +296,7 @@ public final class Kem { // NOPMD
|
||||
|
||||
// ChaCha payload
|
||||
if (wantChaCha) {
|
||||
ChaChaDataContentBuilder cc = ChaChaDataContentBuilder.builder();
|
||||
ChaChaDataContentBuilder cc = ChaChaDataContentBuilder.builder(session);
|
||||
byte[] nonce = parseHexOpt(cmd, OPT_CHACHA_NONCE);
|
||||
if (nonce != null) {
|
||||
cc = cc.withNonce(nonce);
|
||||
|
||||
@@ -42,6 +42,7 @@ import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
@@ -61,10 +62,11 @@ import org.apache.commons.cli.ParseException;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyOperation;
|
||||
import zeroecho.core.KeyOperationInfo;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.SymmetricKeyBuilder;
|
||||
import zeroecho.core.storage.KeyringStore;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
|
||||
/**
|
||||
* Command-line utility for managing key material in a text-based keyring store.
|
||||
@@ -231,6 +233,7 @@ public final class KeyStoreManagement { // NOPMD
|
||||
* @throws IOException if I/O fails
|
||||
*/
|
||||
public static int main(final String[] args, final Options dispatcherOptions) throws ParseException, IOException {
|
||||
ZeroEchoSession session = new ZeroEchoSession();
|
||||
defineOptions(dispatcherOptions);
|
||||
CommandLineParser parser = new DefaultParser();
|
||||
CommandLine cmd = parser.parse(dispatcherOptions, args);
|
||||
@@ -245,14 +248,15 @@ public final class KeyStoreManagement { // NOPMD
|
||||
}
|
||||
|
||||
Path keyringPath = Path.of(cmd.getOptionValue(KEYSTORE_OPTION.getLongOpt()));
|
||||
KeyringStore store = Files.exists(keyringPath) ? KeyringStore.load(keyringPath) : new KeyringStore();
|
||||
KeyringStore store = Files.exists(keyringPath) ? KeyringStore.load(session, keyringPath)
|
||||
: new KeyringStore(session);
|
||||
|
||||
if (cmd.hasOption(LIST_ALIASES_OPTION.getLongOpt())) {
|
||||
listAliases(store);
|
||||
return 0;
|
||||
}
|
||||
if (cmd.hasOption(GENERATE_OPTION.getLongOpt())) {
|
||||
doGenerate(store, cmd);
|
||||
doGenerate(session, store, cmd);
|
||||
store.save(keyringPath);
|
||||
return 0;
|
||||
}
|
||||
@@ -311,8 +315,12 @@ public final class KeyStoreManagement { // NOPMD
|
||||
Set<String> ids = CryptoAlgorithms.available();
|
||||
for (String id : ids) {
|
||||
CryptoAlgorithm a = CryptoAlgorithms.require(id);
|
||||
boolean hasAsym = !a.asymmetricBuildersInfo().isEmpty();
|
||||
boolean hasSym = !a.symmetricBuildersInfo().isEmpty();
|
||||
boolean hasAsym = a.keyOperations().stream()
|
||||
.anyMatch(info -> info.operation() != KeyOperation.SYMMETRIC_GENERATE
|
||||
&& info.operation() != KeyOperation.SYMMETRIC_IMPORT);
|
||||
boolean hasSym = a.keyOperations().stream()
|
||||
.anyMatch(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE
|
||||
|| info.operation() == KeyOperation.SYMMETRIC_IMPORT);
|
||||
out.printf(Locale.ROOT, "%-12s asym:%s sym:%s%n", id, hasAsym, hasSym);
|
||||
}
|
||||
}
|
||||
@@ -346,7 +354,8 @@ public final class KeyStoreManagement { // NOPMD
|
||||
* @param store keyring store to mutate
|
||||
* @param cmd parsed command line
|
||||
*/
|
||||
public static void doGenerate(final KeyringStore store, final CommandLine cmd) { // NOPMD
|
||||
public static void doGenerate(final ZeroEchoSession session, final KeyringStore store,
|
||||
final CommandLine cmd) {
|
||||
String algId = required(cmd, ALG_OPTION, "--alg is required for --generate");
|
||||
String aliasBase = required(cmd, ALIAS_OPTION, "--alias is required for --generate");
|
||||
String kind = cmd.getOptionValue(KIND_OPTION.getLongOpt());
|
||||
@@ -355,119 +364,155 @@ public final class KeyStoreManagement { // NOPMD
|
||||
boolean overwrite = cmd.hasOption(OVERWRITE_OPTION.getLongOpt());
|
||||
|
||||
CryptoAlgorithm alg = CryptoAlgorithms.require(algId);
|
||||
boolean canAsym = !alg.asymmetricBuildersInfo().isEmpty();
|
||||
boolean canSym = !alg.symmetricBuildersInfo().isEmpty();
|
||||
boolean canAsym = alg.keyOperations().stream()
|
||||
.anyMatch(info -> info.operation() == KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE);
|
||||
boolean canSym = alg.keyOperations().stream()
|
||||
.anyMatch(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE);
|
||||
|
||||
boolean doAsym = "asym".equalsIgnoreCase(kind) || (kind == null && canAsym && !canSym);
|
||||
boolean doSym = "sym".equalsIgnoreCase(kind) || (kind == null && canSym && !canAsym);
|
||||
GenerationKind generationKind = selectGenerationKind(kind, canAsym, canSym);
|
||||
if (generationKind == GenerationKind.ASYMMETRIC) {
|
||||
generateAsymmetric(session, store, alg, algId, aliasBase, pubSfx, prvSfx, overwrite);
|
||||
}
|
||||
if (generationKind == GenerationKind.SYMMETRIC) {
|
||||
generateSymmetric(session, store, alg, algId, aliasBase, overwrite);
|
||||
}
|
||||
}
|
||||
|
||||
if (!doAsym && !doSym && canAsym && canSym) {
|
||||
private static GenerationKind selectGenerationKind(String requestedKind, boolean canAsymmetric,
|
||||
boolean canSymmetric) {
|
||||
if ("asym".equalsIgnoreCase(requestedKind) || requestedKind == null && canAsymmetric && !canSymmetric) {
|
||||
return GenerationKind.ASYMMETRIC;
|
||||
}
|
||||
if ("sym".equalsIgnoreCase(requestedKind) || requestedKind == null && canSymmetric && !canAsymmetric) {
|
||||
return GenerationKind.SYMMETRIC;
|
||||
}
|
||||
if (canAsymmetric && canSymmetric) {
|
||||
throw new IllegalArgumentException("Algorithm supports both; specify --kind sym|asym");
|
||||
}
|
||||
return GenerationKind.NONE;
|
||||
}
|
||||
|
||||
if (doAsym) {
|
||||
KeyPair kp = null;
|
||||
CryptoAlgorithm.AsymBuilderInfo used = null;
|
||||
for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) {
|
||||
if (bi.defaultKeySpec == null) {
|
||||
continue;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<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);
|
||||
private static void generateAsymmetric(ZeroEchoSession session, KeyringStore store, CryptoAlgorithm algorithm,
|
||||
String algorithmId, String aliasBase, String publicSuffix, String privateSuffix, boolean overwrite) {
|
||||
GeneratedKeyPair generated = firstGeneratedKeyPair(session, algorithm, algorithmId);
|
||||
Class<?> publicImport = findImportSpecClass(algorithm, KeyOperation.ASYMMETRIC_PUBLIC_IMPORT, true);
|
||||
Class<?> privateImport = findImportSpecClass(algorithm, KeyOperation.ASYMMETRIC_PRIVATE_IMPORT, false);
|
||||
if (publicImport == null && privateImport == null) {
|
||||
throw new IllegalStateException("No import spec class found for " + algorithmId + " (asymmetric)");
|
||||
}
|
||||
|
||||
if (doSym) {
|
||||
SecretKey sk = null;
|
||||
CryptoAlgorithm.SymBuilderInfo used = null;
|
||||
for (CryptoAlgorithm.SymBuilderInfo bi : alg.symmetricBuildersInfo()) {
|
||||
if (bi.defaultKeySpec() == null) {
|
||||
continue;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<AlgorithmKeySpec> st = (Class<AlgorithmKeySpec>) bi.specType();
|
||||
SymmetricKeyBuilder<AlgorithmKeySpec> b = alg.symmetricKeyBuilder(st);
|
||||
try {
|
||||
sk = b.generateSecret((AlgorithmKeySpec) bi.defaultKeySpec());
|
||||
if (sk != null) {
|
||||
used = bi;
|
||||
break;
|
||||
}
|
||||
} catch (Throwable ignore) { // NOPMD
|
||||
KeyPair pair = generated.pair();
|
||||
byte[] publicEncoding = pair.getPublic() == null ? null : pair.getPublic().getEncoded();
|
||||
byte[] privateEncoding = pair.getPrivate() == null ? null : pair.getPrivate().getEncoded();
|
||||
AlgorithmKeySpec publicSpec = publicImport == null ? null
|
||||
: makeImportSpec(publicImport, publicEncoding, algorithmId, generated.info().defaultSpec());
|
||||
AlgorithmKeySpec privateSpec = privateImport == null ? null
|
||||
: makeImportSpec(privateImport, privateEncoding, algorithmId, generated.info().defaultSpec());
|
||||
requireImportSpec(publicImport, publicSpec, "public", algorithmId);
|
||||
requireImportSpec(privateImport, privateSpec, "private", algorithmId);
|
||||
|
||||
String publicAlias = aliasBase + publicSuffix;
|
||||
String privateAlias = aliasBase + privateSuffix;
|
||||
ensureWritable(store, publicAlias, overwrite);
|
||||
ensureWritable(store, privateAlias, overwrite);
|
||||
store.putPublic(publicAlias, algorithmId, publicSpec);
|
||||
store.putPrivate(privateAlias, algorithmId, privateSpec);
|
||||
|
||||
PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD
|
||||
out.printf("Generated %s -> %s, %s%n", algorithmId, publicAlias, privateAlias);
|
||||
}
|
||||
|
||||
private static GeneratedKeyPair firstGeneratedKeyPair(ZeroEchoSession session, CryptoAlgorithm algorithm,
|
||||
String algorithmId) {
|
||||
for (KeyOperationInfo info : algorithm.keyOperations()) {
|
||||
if (info.operation() != KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE || info.defaultSpec() == null) {
|
||||
continue;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<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) {
|
||||
for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) {
|
||||
String n = x.specType().getSimpleName();
|
||||
if (n.contains("Import") || n.endsWith("KeyImportSpec") || n.endsWith("SecretSpec")) {
|
||||
for (KeyOperationInfo x : alg.keyOperations()) {
|
||||
if (x.operation() == KeyOperation.SYMMETRIC_IMPORT) {
|
||||
return x.specType();
|
||||
}
|
||||
}
|
||||
for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) {
|
||||
try {
|
||||
x.specType().getConstructor(byte[].class);
|
||||
return x.specType();
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ import zeroecho.core.spec.VoidSpec;
|
||||
import zeroecho.core.storage.KeyringStore;
|
||||
import zeroecho.core.tag.TagEngineBuilder;
|
||||
import zeroecho.sdk.builders.TagTrailerDataContentBuilder;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
import zeroecho.sdk.content.api.DataContent;
|
||||
import zeroecho.sdk.content.api.PlainContent;
|
||||
import zeroecho.sdk.content.builtin.PlainFile;
|
||||
@@ -157,6 +158,7 @@ public final class Tag { // NOPMD
|
||||
* signature or digest processing
|
||||
*/
|
||||
public static int main(String[] args, Options root) throws ParseException, IOException, GeneralSecurityException {
|
||||
ZeroEchoSession session = new ZeroEchoSession();
|
||||
Options opts = root;
|
||||
opts.addOption(TYPE_OPT);
|
||||
opts.addOption(MODE_OPT);
|
||||
@@ -196,21 +198,23 @@ public final class Tag { // NOPMD
|
||||
|
||||
if (TYPE_SIGNATURE.equals(type)) {
|
||||
String ksPath = require(cli, KS_OPT, "--ks <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;
|
||||
|
||||
if (produce) {
|
||||
String privAlias = require(cli, PRIV_OPT, "signature produce requires --priv <alias>");
|
||||
PrivateKey priv = keyring.getPrivate(privAlias);
|
||||
tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(alg, priv, spec)).build(true);
|
||||
tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(session, alg, priv, spec))
|
||||
.build(true);
|
||||
} else {
|
||||
String pubAlias = require(cli, PUB_OPT, "signature verify requires --pub <alias>");
|
||||
PublicKey pub = keyring.getPublic(pubAlias);
|
||||
tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(alg, pub, spec)).build(false);
|
||||
tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(session, alg, pub, spec))
|
||||
.build(false);
|
||||
}
|
||||
} else { // digest
|
||||
DigestSpec spec = parseDigest(alg);
|
||||
tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(spec)).build(produce);
|
||||
tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(session, spec)).build(produce);
|
||||
}
|
||||
|
||||
tail.setInput(source);
|
||||
|
||||
@@ -36,6 +36,7 @@ package zeroecho;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.nio.file.Files;
|
||||
@@ -45,6 +46,7 @@ import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
import org.apache.commons.cli.Options;
|
||||
import org.apache.commons.cli.ParseException;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -74,6 +76,7 @@ import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
* </p>
|
||||
*/
|
||||
public class GuardTest {
|
||||
private static final String TEST_PBKDF2_MAXIMUM = "1000000";
|
||||
|
||||
/** All temporary files live here and are auto-cleaned by JUnit. */
|
||||
@TempDir
|
||||
@@ -120,14 +123,16 @@ public class GuardTest {
|
||||
Path dec = tmp.resolve("pt.bin.dec");
|
||||
|
||||
// Encrypt
|
||||
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", password, "--alg",
|
||||
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", password,
|
||||
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg",
|
||||
"aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aadHex };
|
||||
System.out.println("...encrypt: " + Arrays.toString(encArgs));
|
||||
int e = Guard.main(encArgs, new Options());
|
||||
assertEquals(0, e, "... encrypt expected exit code 0");
|
||||
|
||||
// Decrypt (using password)
|
||||
String[] decArgs = { "--decrypt", enc.toString(), "--output", dec.toString(), "--password", password, "--alg",
|
||||
String[] decArgs = { "--decrypt", enc.toString(), "--output", dec.toString(), "--password", password,
|
||||
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg",
|
||||
"aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aadHex };
|
||||
System.out.println("...decrypt: " + Arrays.toString(decArgs));
|
||||
int d = Guard.main(decArgs, new Options());
|
||||
@@ -137,6 +142,42 @@ public class GuardTest {
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void passwordOperationRequiresExplicitLimits() throws Exception {
|
||||
String method = "passwordOperationRequiresExplicitLimits";
|
||||
System.out.println(method);
|
||||
Path input = writeRandom(tmp.resolve("limits.bin"), 32, 0x51A17);
|
||||
String[] arguments = { "--encrypt", input.toString(), "--to-psw", "controlled", "--alg", "aes-gcm" };
|
||||
|
||||
ParseException failure = assertThrows(ParseException.class,
|
||||
() -> Guard.main(arguments, new Options()));
|
||||
|
||||
assertTrue(failure.getMessage().contains("--pbkdf2-max"));
|
||||
System.out.println("...rejected=missingLimits");
|
||||
System.out.println(method + "...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void recipientKekOptionRejectsUnreadableSize() throws Exception {
|
||||
String method = "recipientKekOptionRejectsUnreadableSize";
|
||||
System.out.println(method);
|
||||
Path input = writeRandom(tmp.resolve("invalid-kek.bin"), 32, 0x4B454B);
|
||||
Path output = tmp.resolve("invalid-kek.enc");
|
||||
String[] arguments = { "--encrypt", input.toString(), "--output", output.toString(),
|
||||
"--to-psw", "controlled", "--to-kek-bytes", "24",
|
||||
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM,
|
||||
"--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM,
|
||||
"--alg", "aes-gcm" };
|
||||
|
||||
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
|
||||
() -> Guard.main(arguments, new Options()));
|
||||
|
||||
assertTrue(failure.getMessage().contains("exactly 16 or 32"));
|
||||
assertTrue(Files.notExists(output));
|
||||
System.out.println("...rejectedKekBytes=24");
|
||||
System.out.println(method + "...ok");
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA recipient round trips with both AES-GCM and ChaCha20-Poly1305 payloads.
|
||||
*
|
||||
@@ -249,7 +290,8 @@ public class GuardTest {
|
||||
// alias,
|
||||
// plus 2 random password decoys. Recipients are shuffled by default.
|
||||
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--keyring", ring.toString(),
|
||||
"--to-alias", rsa.pub, "--to-psw", password, "--decoy-alias", elg.pub, "--decoy-psw-rand", "2", "--alg",
|
||||
"--to-alias", rsa.pub, "--to-psw", password, "--decoy-alias", elg.pub, "--decoy-psw-rand", "2",
|
||||
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg",
|
||||
"aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aad };
|
||||
System.out.println("...encrypt: " + Arrays.toString(encArgs));
|
||||
int e = Guard.main(encArgs, new Options());
|
||||
@@ -266,7 +308,8 @@ public class GuardTest {
|
||||
"mixed recipients decrypt(private) mismatch");
|
||||
|
||||
// Decrypt via password instead of key
|
||||
String[] decPwd = { "--decrypt", enc.toString(), "--output", dec2.toString(), "--password", password, "--alg",
|
||||
String[] decPwd = { "--decrypt", enc.toString(), "--output", dec2.toString(), "--password", password,
|
||||
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg",
|
||||
"aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aad };
|
||||
System.out.println("...decrypt(password): " + Arrays.toString(decPwd));
|
||||
int d2 = Guard.main(decPwd, new Options());
|
||||
@@ -292,7 +335,8 @@ public class GuardTest {
|
||||
Path enc = tmp.resolve("pt-neg.bin.enc");
|
||||
String pwd = "x";
|
||||
|
||||
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", pwd, "--alg",
|
||||
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", pwd,
|
||||
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg",
|
||||
"aes-gcm", "--tag-bits", "128" };
|
||||
int e = Guard.main(encArgs, new Options());
|
||||
assertEquals(0, e, "... encrypt rc");
|
||||
|
||||
@@ -166,7 +166,7 @@ public class KemTest {
|
||||
KeyAliases aliases = generateKemIntoKeyStore(ring, kemId, "alias-" + shortId(kemId));
|
||||
|
||||
// Sanity: re-open to ensure the file is valid
|
||||
KeyringStore ks = KeyringStore.load(ring);
|
||||
KeyringStore ks = KeyringStore.load(new zeroecho.sdk.ZeroEchoSession(), ring);
|
||||
if (!(ks.contains(aliases.pub) && ks.contains(aliases.prv))) {
|
||||
throw new IllegalStateException("Keyring does not contain expected aliases for " + kemId);
|
||||
}
|
||||
|
||||
@@ -52,10 +52,9 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.SymmetricKeyBuilder;
|
||||
import zeroecho.core.KeyOperation;
|
||||
import zeroecho.core.storage.KeyringStore;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
/**
|
||||
@@ -91,6 +90,7 @@ public class KeyStoreManagementTest {
|
||||
@Test
|
||||
public void generateAndVerifyAllAlgorithms() throws Exception {
|
||||
Path ring = tmp.resolve("ring.txt");
|
||||
ZeroEchoSession session = new ZeroEchoSession();
|
||||
|
||||
Set<String> algIds = CryptoAlgorithms.available();
|
||||
System.out.println("Algorithms: " + algIds);
|
||||
@@ -139,7 +139,7 @@ public class KeyStoreManagementTest {
|
||||
assertTrue(attempted > 0, "No generation attempts were successful");
|
||||
|
||||
// Verify by reloading and materializing.
|
||||
KeyringStore store = KeyringStore.load(ring);
|
||||
KeyringStore store = KeyringStore.load(session, ring);
|
||||
List<String> aliases = store.aliases();
|
||||
System.out.println("Reloaded aliases (" + aliases.size() + "): " + aliases);
|
||||
|
||||
@@ -189,45 +189,15 @@ public class KeyStoreManagementTest {
|
||||
// ---- helpers ----
|
||||
|
||||
private static boolean hasAsymmetricDefault(CryptoAlgorithm alg) {
|
||||
try {
|
||||
List<CryptoAlgorithm.AsymBuilderInfo> infos = alg.asymmetricBuildersInfo();
|
||||
for (int i = 0; i < infos.size(); i++) {
|
||||
CryptoAlgorithm.AsymBuilderInfo bi = infos.get(i);
|
||||
if (bi.defaultKeySpec == null) {
|
||||
continue;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<AlgorithmKeySpec> st = (Class<AlgorithmKeySpec>) bi.specType;
|
||||
AsymmetricKeyBuilder<AlgorithmKeySpec> b = alg.asymmetricKeyBuilder(st);
|
||||
if (b != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
return alg.keyOperations().stream()
|
||||
.anyMatch(info -> info.operation() == KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE
|
||||
&& info.defaultSpec() != null);
|
||||
}
|
||||
|
||||
private static boolean hasSymmetricDefault(CryptoAlgorithm alg) {
|
||||
try {
|
||||
List<CryptoAlgorithm.SymBuilderInfo> infos = alg.symmetricBuildersInfo();
|
||||
for (int i = 0; i < infos.size(); i++) {
|
||||
CryptoAlgorithm.SymBuilderInfo bi = infos.get(i);
|
||||
if (bi.defaultKeySpec() == null) {
|
||||
continue;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<AlgorithmKeySpec> st = (Class<AlgorithmKeySpec>) bi.specType();
|
||||
SymmetricKeyBuilder<AlgorithmKeySpec> b = alg.symmetricKeyBuilder(st);
|
||||
if (b != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
return alg.keyOperations().stream()
|
||||
.anyMatch(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE
|
||||
&& info.defaultSpec() != null);
|
||||
}
|
||||
|
||||
private static String sanitize(String id) {
|
||||
|
||||
@@ -127,7 +127,7 @@ public class TagTest {
|
||||
Path ring = tmp.resolve("ring-ed25519.txt");
|
||||
KeyAliases ed = generateIntoKeyStore(ring, "Ed25519", "ed");
|
||||
// sanity
|
||||
KeyringStore ks = KeyringStore.load(ring);
|
||||
KeyringStore ks = KeyringStore.load(new zeroecho.sdk.ZeroEchoSession(), ring);
|
||||
assertTrue(ks.contains(ed.pub) && ks.contains(ed.prv), "missing expected aliases");
|
||||
|
||||
byte[] pt = randomBytes(4096);
|
||||
|
||||
@@ -55,13 +55,13 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import conflux.Ctx;
|
||||
import conflux.CtxInterface;
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.alg.aes.AesKeyGenSpec;
|
||||
import zeroecho.core.alg.aes.AesSpec;
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
import zeroecho.sdk.builders.core.DataContentChainBuilder;
|
||||
import zeroecho.sdk.builders.core.PlainBytesBuilder;
|
||||
import zeroecho.sdk.content.api.DataContent;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
|
||||
class JpegExifIntegrationTest {
|
||||
|
||||
@@ -91,17 +91,17 @@ class JpegExifIntegrationTest {
|
||||
|
||||
// AES encryption setup
|
||||
/*
|
||||
* CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); SecretKey key =
|
||||
* aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.
|
||||
* aes256()); AesSpec spec =
|
||||
* SecretKey key = zeroEchoSession.keyBuilders().symmetric()
|
||||
* .generate("AES", AesKeyGenSpec.aes256()); AesSpec spec =
|
||||
* AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build();
|
||||
* EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key,
|
||||
* EncryptionContext enc = zeroEchoSession.createContext("AES", KeyUsage.ENCRYPT, key,
|
||||
* spec); CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" +
|
||||
* System.nanoTime()); session.put(ConfluxKeys.aad("AES"), aad); ((ContextAware)
|
||||
* enc).setContext(session);
|
||||
*/
|
||||
SecretKey key = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class)
|
||||
.generateSecret(AesKeyGenSpec.aes256());
|
||||
ZeroEchoSession zeroEchoSession = new ZeroEchoSession();
|
||||
SecretKey key = zeroEchoSession.keyBuilders().symmetric()
|
||||
.generate("AES", AesKeyGenSpec.aes256());
|
||||
CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime());
|
||||
|
||||
byte[] encryptedBytes;
|
||||
@@ -109,7 +109,7 @@ class JpegExifIntegrationTest {
|
||||
// input
|
||||
.add(PlainBytesBuilder.builder().bytes(inputBytes))
|
||||
// encryption
|
||||
.add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded())
|
||||
.add(AesDataContentBuilder.builder(zeroEchoSession).importKeyRaw(key.getEncoded())
|
||||
// using general AES/GCM/128 without specified header
|
||||
.spec(AesSpec.gcm128(null))
|
||||
// but let the builder add the default header for storing AAD and IV
|
||||
@@ -152,7 +152,7 @@ class JpegExifIntegrationTest {
|
||||
// input
|
||||
.add(PlainBytesBuilder.builder().bytes(extractedEncryptedBytes))
|
||||
// encryption
|
||||
.add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()).spec(AesSpec.gcm128(null))
|
||||
.add(AesDataContentBuilder.builder(zeroEchoSession).importKeyRaw(key.getEncoded()).spec(AesSpec.gcm128(null))
|
||||
// let us use the default header for AAD and IV
|
||||
.withHeader().withAad(aad).context(session))
|
||||
// and create the pipeline
|
||||
@@ -164,7 +164,7 @@ class JpegExifIntegrationTest {
|
||||
/*
|
||||
* AesSpec spec =
|
||||
* AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build();
|
||||
* EncryptionContext dec1 = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT,
|
||||
* EncryptionContext dec1 = zeroEchoSession.createContext("AES", KeyUsage.DECRYPT,
|
||||
* key, spec); ((ContextAware) dec1).setContext(session); // same IV/AAD in ctx
|
||||
* byte[] pt1 = readAll(dec1.attach(new
|
||||
* ByteArrayInputStream(extractedEncryptedBytes))); dec1.close();
|
||||
|
||||
@@ -1,105 +1,55 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
* are permitted provided that the conditions in the project LICENSE are met.
|
||||
******************************************************************************/
|
||||
package zeroecho.core;
|
||||
|
||||
import java.security.Key;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||
import zeroecho.core.context.CryptoContext;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
import zeroecho.core.spi.ContextConstructorKS;
|
||||
|
||||
/**
|
||||
* Immutable descriptor of an algorithm capability.
|
||||
* Immutable value descriptor of one algorithm context capability.
|
||||
*
|
||||
* <p>
|
||||
* A {@code Capability} describes one role supported by a
|
||||
* {@link CryptoAlgorithm}, including:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>the algorithm identifier,</li>
|
||||
* <li>its high-level {@link AlgorithmFamily},</li>
|
||||
* <li>the {@link KeyUsage} role (e.g., ENCRYPT, VERIFY),</li>
|
||||
* <li>the expected {@link CryptoContext} type,</li>
|
||||
* <li>the accepted {@link Key} type,</li>
|
||||
* <li>the accepted {@link ContextSpec} type, and</li>
|
||||
* <li>a supplier for a default spec.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Purpose</h2> Capabilities allow discovery, inspection, and documentation
|
||||
* of what an algorithm can do. Higher layers (e.g., protocol builders,
|
||||
* registries, tooling) can enumerate capabilities via
|
||||
* {@link CryptoAlgorithm#listCapabilities()} and adapt automatically.
|
||||
*
|
||||
* <p>
|
||||
* Each capability corresponds to a call to
|
||||
* {@link AbstractCryptoAlgorithm#capability(AlgorithmFamily, KeyUsage, Class, Class, Class, ContextConstructorKS, Supplier)}.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Thread-safety</h2> {@code Capability} instances are immutable and safe to
|
||||
* share across threads.
|
||||
* <p>The default specification is resolved once during provider construction.
|
||||
* All components therefore have stable value semantics and are safe for
|
||||
* concurrent reads.</p>
|
||||
*
|
||||
* @param algorithmId canonical algorithm identifier
|
||||
* @param family algorithm family
|
||||
* @param role supported key usage
|
||||
* @param contextType produced context type
|
||||
* @param keyType accepted key type
|
||||
* @param specType accepted specification type
|
||||
* @param defaultSpec non-null resolved default specification
|
||||
* @since 1.0
|
||||
*/
|
||||
public record Capability(String algorithmId, AlgorithmFamily family, KeyUsage role,
|
||||
Class<? extends CryptoContext> contextType, Class<? extends Key> keyType, Class<? extends ContextSpec> specType,
|
||||
Supplier<? extends ContextSpec> defaultSpec) {
|
||||
Class<? extends CryptoContext> contextType, Class<? extends Key> keyType,
|
||||
Class<? extends ContextSpec> specType, ContextSpec defaultSpec) {
|
||||
|
||||
/**
|
||||
* Creates a new capability descriptor.
|
||||
* Validates the capability metadata.
|
||||
*
|
||||
* @param algorithmId identifier of the algorithm this capability belongs to
|
||||
* @param family high-level algorithm family classification
|
||||
* @param role supported {@link KeyUsage} role
|
||||
* @param contextType expected {@link CryptoContext} type for this role
|
||||
* @param keyType accepted {@link Key} type for this role
|
||||
* @param specType accepted {@link ContextSpec} type for this role
|
||||
* @param defaultSpec supplier of a default spec (used when {@code null} is
|
||||
* passed)
|
||||
* @throws NullPointerException if any argument is {@code null}
|
||||
* @throws NullPointerException if a component is {@code null}
|
||||
* @throws IllegalArgumentException if {@code defaultSpec} is incompatible
|
||||
* with {@code specType}
|
||||
*/
|
||||
public Capability(String algorithmId, AlgorithmFamily family, KeyUsage role,
|
||||
Class<? extends CryptoContext> contextType, Class<? extends Key> keyType,
|
||||
Class<? extends ContextSpec> specType, Supplier<? extends ContextSpec> defaultSpec) {
|
||||
this.algorithmId = Objects.requireNonNull(algorithmId, "algorithmId must not be null");
|
||||
this.family = Objects.requireNonNull(family, "family must not be null");
|
||||
this.role = Objects.requireNonNull(role, "role must not be null");
|
||||
this.contextType = Objects.requireNonNull(contextType, "contextType must not be null");
|
||||
this.keyType = Objects.requireNonNull(keyType, "keyType must not be null");
|
||||
this.specType = Objects.requireNonNull(specType, "specType must not be null");
|
||||
this.defaultSpec = Objects.requireNonNull(defaultSpec, "defaultSpec must not be null");
|
||||
public Capability {
|
||||
Objects.requireNonNull(algorithmId, "algorithmId must not be null");
|
||||
Objects.requireNonNull(family, "family must not be null");
|
||||
Objects.requireNonNull(role, "role must not be null");
|
||||
Objects.requireNonNull(contextType, "contextType must not be null");
|
||||
Objects.requireNonNull(keyType, "keyType must not be null");
|
||||
Objects.requireNonNull(specType, "specType must not be null");
|
||||
Objects.requireNonNull(defaultSpec, "defaultSpec must not be null");
|
||||
if (!specType.isInstance(defaultSpec)) {
|
||||
throw new IllegalArgumentException("defaultSpec must be an instance of " + specType.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,32 +33,29 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import zeroecho.core.context.CryptoContext;
|
||||
import zeroecho.core.err.UnsupportedRoleException;
|
||||
import zeroecho.core.err.UnsupportedSpecException;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.ContextConstructorKS;
|
||||
import zeroecho.core.spi.SymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.AsymmetricKeyPairGenerator;
|
||||
import zeroecho.core.spi.ContextFactoryKS;
|
||||
import zeroecho.core.spi.PrivateKeyImporter;
|
||||
import zeroecho.core.spi.PublicKeyImporter;
|
||||
import zeroecho.core.spi.SymmetricKeyGenerator;
|
||||
import zeroecho.core.spi.SymmetricKeyImporter;
|
||||
|
||||
/**
|
||||
* Abstract base class for all cryptographic algorithm definitions in ZeroEcho.
|
||||
@@ -70,8 +67,7 @@ import zeroecho.core.spi.SymmetricKeyBuilder;
|
||||
* signatures.</li>
|
||||
* <li>Roles: supported {@link KeyUsage} operations (e.g., ENCRYPT, SIGN) bound
|
||||
* to concrete {@link CryptoContext} constructors.</li>
|
||||
* <li>Key builders: factories for symmetric and asymmetric key material via
|
||||
* {@link SymmetricKeyBuilder} and {@link AsymmetricKeyBuilder}.</li>
|
||||
* <li>Key operations: exact generation and import capabilities.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Metadata</h2> Each algorithm instance is uniquely identified by
|
||||
@@ -86,7 +82,7 @@ import zeroecho.core.spi.SymmetricKeyBuilder;
|
||||
* <h2>Roles and contexts</h2> Each algorithm may support multiple
|
||||
* {@link KeyUsage} roles. For each role, the algorithm binds a key type,
|
||||
* context type, and optional {@link ContextSpec}. When
|
||||
* {@link #create(KeyUsage, Key, ContextSpec)} is called:
|
||||
* {@link #createContext(KeyUsage, Key, ContextSpec)} is called:
|
||||
* <ol>
|
||||
* <li>The binding for the role is located.</li>
|
||||
* <li>The supplied key and spec are validated against the expected types.</li>
|
||||
@@ -94,15 +90,9 @@ import zeroecho.core.spi.SymmetricKeyBuilder;
|
||||
* factory.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h2>Key builders</h2>
|
||||
* <ul>
|
||||
* <li>Asymmetric builders: registered via {@link #registerAsymmetricKeyBuilder}
|
||||
* and accessed through {@link #asymmetricKeyBuilder(Class)} or convenience
|
||||
* methods like {@link #generateKeyPair(AlgorithmKeySpec)}.</li>
|
||||
* <li>Symmetric builders: registered via {@link #registerSymmetricKeyBuilder}
|
||||
* and accessed through {@link #symmetricKeyBuilder(Class)} or convenience
|
||||
* methods like {@link #generateSecret(AlgorithmKeySpec)}.</li>
|
||||
* </ul>
|
||||
* <h2>Key operations</h2> Providers register generation and import operations
|
||||
* independently. Lookup returns an interface that guarantees the requested
|
||||
* operation.
|
||||
*
|
||||
* <h2>Provider model</h2> Each algorithm belongs to a {@code providerName},
|
||||
* allowing multiple providers (e.g., JCA, BouncyCastle, ZeroEcho-native) to
|
||||
@@ -115,7 +105,8 @@ import zeroecho.core.spi.SymmetricKeyBuilder;
|
||||
*
|
||||
* <p>
|
||||
* <b>Security note:</b> Algorithms must enforce strong validation of keys and
|
||||
* specs during registration and {@link #create(KeyUsage, Key, ContextSpec)} to
|
||||
* specs during registration and
|
||||
* {@link #createContext(KeyUsage, Key, ContextSpec)} to
|
||||
* prevent downgrade or misuse attacks.
|
||||
* </p>
|
||||
*
|
||||
@@ -123,6 +114,8 @@ import zeroecho.core.spi.SymmetricKeyBuilder;
|
||||
*/
|
||||
public abstract class CryptoAlgorithm { // NOPMD
|
||||
|
||||
private static final String SPEC_TYPE_NULL = "specType must not be null";
|
||||
|
||||
private final String _id;
|
||||
private final String _displayName;
|
||||
private final int _priority;
|
||||
@@ -130,8 +123,18 @@ public abstract class CryptoAlgorithm { // NOPMD
|
||||
|
||||
private final List<Capability> capabilities = new ArrayList<>();
|
||||
private final Map<KeyUsage, List<RoleBinding<?, ?, ?>>> ctxBindings = new EnumMap<>(KeyUsage.class);
|
||||
private final Map<Class<? extends AlgorithmKeySpec>, AsymEntry<?>> asymBuilders = new HashMap<>();
|
||||
private final Map<Class<? extends AlgorithmKeySpec>, SymEntry<?>> symBuilders = new HashMap<>();
|
||||
private final Map<Class<? extends AlgorithmKeySpec>, AsymmetricKeyPairGenerator<?>> keyPairGenerators =
|
||||
new LinkedHashMap<>();
|
||||
private final Map<Class<? extends AlgorithmKeySpec>, PublicKeyImporter<?>> publicKeyImporters =
|
||||
new LinkedHashMap<>();
|
||||
private final Map<Class<? extends AlgorithmKeySpec>, PrivateKeyImporter<?>> privateKeyImporters =
|
||||
new LinkedHashMap<>();
|
||||
private final Map<Class<? extends AlgorithmKeySpec>, SymmetricKeyGenerator<?>> symmetricKeyGenerators =
|
||||
new LinkedHashMap<>();
|
||||
private final Map<Class<? extends AlgorithmKeySpec>, SymmetricKeyImporter<?>> symmetricKeyImporters =
|
||||
new LinkedHashMap<>();
|
||||
private final Map<Class<? extends AlgorithmKeySpec>, AlgorithmKeySpec> asymmetricDefaults = new LinkedHashMap<>();
|
||||
private final Map<Class<? extends AlgorithmKeySpec>, AlgorithmKeySpec> symmetricDefaults = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Create a new algorithm with default priority and provider.
|
||||
@@ -270,15 +273,15 @@ public abstract class CryptoAlgorithm { // NOPMD
|
||||
private final Class<C> ctxType;
|
||||
private final Class<K> keyType;
|
||||
private final Class<S> specType;
|
||||
private final ContextConstructorKS<C, K, S> ctor;
|
||||
private final ContextFactoryKS<C, K, S> factory;
|
||||
private final Supplier<? extends S> defaultSpec;
|
||||
|
||||
private RoleBinding(Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextConstructorKS<C, K, S> ctor,
|
||||
private RoleBinding(Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextFactoryKS<C, K, S> factory,
|
||||
Supplier<? extends S> defaultSpec) {
|
||||
this.ctxType = ctxType;
|
||||
this.keyType = keyType;
|
||||
this.specType = specType;
|
||||
this.ctor = ctor;
|
||||
this.factory = factory;
|
||||
this.defaultSpec = defaultSpec;
|
||||
}
|
||||
|
||||
@@ -293,7 +296,7 @@ public abstract class CryptoAlgorithm { // NOPMD
|
||||
* <p>
|
||||
* Concrete algorithms call this during construction to declare support for
|
||||
* specific roles (e.g., {@code ENCRYPT}, {@code VERIFY}). When
|
||||
* {@link #create(KeyUsage, Key, ContextSpec)} is later invoked, the provided
|
||||
* {@link #createContext(KeyUsage, Key, ContextSpec)} is later invoked, the provided
|
||||
* {@code key} and optional {@code spec} are matched against these bindings.
|
||||
* </p>
|
||||
*
|
||||
@@ -309,9 +312,15 @@ public abstract class CryptoAlgorithm { // NOPMD
|
||||
* @param <S> spec type
|
||||
* @throws NullPointerException if any class or factory argument is {@code null}
|
||||
*/
|
||||
protected final <C extends CryptoContext, K extends Key, S extends ContextSpec> void bind(KeyUsage role,
|
||||
Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextConstructorKS<C, K, S> factory,
|
||||
protected final <C extends CryptoContext, K extends Key, S extends ContextSpec> void bindContext(KeyUsage role,
|
||||
Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextFactoryKS<C, K, S> factory,
|
||||
Supplier<? extends S> defaultSpec) {
|
||||
Objects.requireNonNull(role, "role must not be null");
|
||||
Objects.requireNonNull(ctxType, "ctxType must not be null");
|
||||
Objects.requireNonNull(keyType, "keyType must not be null");
|
||||
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
|
||||
Objects.requireNonNull(factory, "factory must not be null");
|
||||
Objects.requireNonNull(defaultSpec, "defaultSpec must not be null");
|
||||
ctxBindings.computeIfAbsent(role, r -> new ArrayList<>())
|
||||
.add(new RoleBinding<>(ctxType, keyType, specType, factory, defaultSpec));
|
||||
}
|
||||
@@ -367,13 +376,18 @@ public abstract class CryptoAlgorithm { // NOPMD
|
||||
* @throws UnsupportedSpecException if no binding accepts the provided key/spec
|
||||
* @throws IllegalStateException if the factory returns an unexpected context
|
||||
* type
|
||||
* @throws IOException if the factory encounters I/O while
|
||||
* constructing the context
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public final <C extends CryptoContext, K extends Key, S extends ContextSpec> C create(KeyUsage role, K key, S spec)
|
||||
throws IOException {
|
||||
public final <C extends CryptoContext, K extends Key, S extends ContextSpec> C createContext(KeyUsage role, K key,
|
||||
S spec) {
|
||||
return createContextInternal(role, key, spec);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <C extends CryptoContext, K extends Key, S extends ContextSpec> C createContextInternal(KeyUsage role,
|
||||
K key, S spec) {
|
||||
Objects.requireNonNull(role, "role must not be null");
|
||||
Objects.requireNonNull(key, "key must not be null");
|
||||
List<RoleBinding<?, ?, ?>> list = ctxBindings.get(role);
|
||||
if (list == null || list.isEmpty()) {
|
||||
throw new UnsupportedRoleException(_id + " does not support role " + role);
|
||||
@@ -381,8 +395,10 @@ public abstract class CryptoAlgorithm { // NOPMD
|
||||
for (RoleBinding<?, ?, ?> rb0 : list) {
|
||||
RoleBinding<C, K, S> rb = (RoleBinding<C, K, S>) rb0;
|
||||
if (rb.accepts(key, spec)) {
|
||||
S resolved = (spec != null) ? spec : rb.defaultSpec.get();
|
||||
C ctx = rb.ctor.create(key, resolved);
|
||||
S resolved = (spec != null) ? spec
|
||||
: Objects.requireNonNull(rb.defaultSpec.get(), "defaultSpec value must not be null");
|
||||
C ctx = Objects.requireNonNull(rb.factory.createContext(key, resolved),
|
||||
_id + " factory returned null");
|
||||
// Enforce the declared context type contract:
|
||||
if (!rb.ctxType.isInstance(ctx)) {
|
||||
throw new IllegalStateException(_id + " factory returned " + ctx.getClass().getName()
|
||||
@@ -395,451 +411,239 @@ public abstract class CryptoAlgorithm { // NOPMD
|
||||
+ (spec == null ? " (default spec)" : " and spec=" + spec.getClass().getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable descriptor for an asymmetric builder registered with this
|
||||
* algorithm.
|
||||
* <p>
|
||||
* Used for discovery and documentation (e.g., tool UIs).
|
||||
* </p>
|
||||
*/
|
||||
public static final class AsymBuilderInfo {
|
||||
public final Class<? extends AlgorithmKeySpec> specType;
|
||||
public final Object defaultKeySpec;
|
||||
|
||||
private AsymBuilderInfo(Class<? extends AlgorithmKeySpec> specType, Object defaultKeySpec) {
|
||||
this.specType = specType;
|
||||
this.defaultKeySpec = defaultKeySpec;
|
||||
private <S extends AlgorithmKeySpec> S resolveDefault(Class<S> specType,
|
||||
Supplier<? extends S> defaultSpecOrNull) {
|
||||
if (defaultSpecOrNull == null) {
|
||||
return null;
|
||||
}
|
||||
S value = Objects.requireNonNull(defaultSpecOrNull.get(), "defaultSpec value must not be null");
|
||||
if (!specType.isInstance(value)) {
|
||||
throw new IllegalArgumentException("defaultSpec must be an instance of " + specType.getName());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal entry binding a registered asymmetric key builder to its default key
|
||||
* specification supplier.
|
||||
* Registers asymmetric key-pair generation for one exact specification class.
|
||||
*
|
||||
* <p>
|
||||
* Each {@code AsymEntry} is keyed by a specific {@link AlgorithmKeySpec}
|
||||
* subtype. It holds the {@link AsymmetricKeyBuilder} instance capable of
|
||||
* generating or importing keys for that spec, and an optional supplier that
|
||||
* provides a safe default spec (if the algorithm wants to support "generate
|
||||
* with defaults").
|
||||
* </p>
|
||||
* <p>The optional default is resolved and validated during registration.
|
||||
* Registered generators must be safe for concurrent invocation after the
|
||||
* algorithm is published.</p>
|
||||
*
|
||||
* <h2>Usage</h2>
|
||||
* <ul>
|
||||
* <li>Created during calls to
|
||||
* {@link #registerAsymmetricKeyBuilder(Class, AsymmetricKeyBuilder, Supplier)}.</li>
|
||||
* <li>Looked up later by {@link #asymmetricKeyBuilder(Class)} and used by
|
||||
* key-generation/import convenience methods such as
|
||||
* {@link #generateKeyPair(AlgorithmKeySpec)}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Thread-safety</h2> Immutable once constructed; safe to share between
|
||||
* threads.
|
||||
*
|
||||
* @param <S> the type of {@link AlgorithmKeySpec} handled by this entry
|
||||
*/
|
||||
private record AsymEntry<S extends AlgorithmKeySpec>(AsymmetricKeyBuilder<S> builder,
|
||||
Supplier<? extends S> defaultKeySpec) {
|
||||
|
||||
/**
|
||||
* Creates a new binding between a key builder and its optional default spec.
|
||||
*
|
||||
* @throws NullPointerException if {@code builder} is {@code null}
|
||||
*/
|
||||
AsymEntry {
|
||||
Objects.requireNonNull(builder, "builder must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an asymmetric key builder for a specific spec type.
|
||||
*
|
||||
* <p>
|
||||
* Concrete algorithms call this during construction. The {@code specType} acts
|
||||
* as a key for later lookup and must be unique within this algorithm.
|
||||
* </p>
|
||||
*
|
||||
* @param specType the spec class accepted by {@code builder}
|
||||
* @param builder builder that can generate/import keys for
|
||||
* {@code specType}
|
||||
* @param defaultKeySpecOrNull optional supplier for a default spec (may be
|
||||
* {@code null})
|
||||
* @param <S> spec type
|
||||
* @throws NullPointerException if {@code specType} or {@code builder} is
|
||||
* @param specType exact specification class
|
||||
* @param generator non-null generator
|
||||
* @param defaultSpecOrNull optional default supplier, evaluated once
|
||||
* @param <S> specification type
|
||||
* @throws NullPointerException if a required argument or supplied default is
|
||||
* {@code null}
|
||||
* @throws IllegalArgumentException if the supplied default has the wrong type
|
||||
*/
|
||||
protected final <S extends AlgorithmKeySpec> void registerAsymmetricKeyBuilder(Class<S> specType,
|
||||
AsymmetricKeyBuilder<S> builder, Supplier<? extends S> defaultKeySpecOrNull) {
|
||||
Objects.requireNonNull(specType, "specType must not be null");
|
||||
asymBuilders.put(specType, new AsymEntry<>(builder, defaultKeySpecOrNull));
|
||||
protected final <S extends AlgorithmKeySpec> void registerAsymmetricKeyPairGenerator(Class<S> specType,
|
||||
AsymmetricKeyPairGenerator<S> generator, Supplier<? extends S> defaultSpecOrNull) {
|
||||
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
|
||||
keyPairGenerators.put(specType, Objects.requireNonNull(generator, "generator must not be null"));
|
||||
asymmetricDefaults.put(specType, resolveDefault(specType, defaultSpecOrNull));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the asymmetric key builder associated with the given spec type.
|
||||
* Registers public-key import for one exact specification class.
|
||||
*
|
||||
* @param specType spec class used as a lookup key
|
||||
* @param <S> spec type
|
||||
* @return the registered {@link AsymmetricKeyBuilder}
|
||||
* @throws IllegalArgumentException if no builder is registered for
|
||||
* {@code specType}
|
||||
* @param specType exact specification class
|
||||
* @param importer non-null importer safe for concurrent invocation
|
||||
* @param <S> specification type
|
||||
* @throws NullPointerException if an argument is {@code null}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public final <S extends AlgorithmKeySpec> AsymmetricKeyBuilder<S> asymmetricKeyBuilder(Class<S> specType) {
|
||||
AsymEntry<?> e = asymBuilders.get(specType);
|
||||
if (e == null) {
|
||||
throw new IllegalArgumentException(_id + " has no asymmetric key builder for " + specType.getName());
|
||||
}
|
||||
return (AsymmetricKeyBuilder<S>) e.builder;
|
||||
protected final <S extends AlgorithmKeySpec> void registerPublicKeyImporter(Class<S> specType,
|
||||
PublicKeyImporter<S> importer) {
|
||||
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
|
||||
publicKeyImporters.put(specType, Objects.requireNonNull(importer, "importer must not be null"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns metadata about all registered asymmetric builders.
|
||||
* Registers private-key import for one exact specification class.
|
||||
*
|
||||
* <p>
|
||||
* The default spec value is best-effort; suppliers may throw, in which case
|
||||
* {@code defaultKeySpec} is reported as {@code null}.
|
||||
* </p>
|
||||
*
|
||||
* @return immutable list of {@link AsymBuilderInfo} descriptors
|
||||
* @param specType exact specification class
|
||||
* @param importer non-null importer safe for concurrent invocation
|
||||
* @param <S> specification type
|
||||
* @throws NullPointerException if an argument is {@code null}
|
||||
*/
|
||||
public final List<AsymBuilderInfo> asymmetricBuildersInfo() {
|
||||
List<AsymBuilderInfo> out = new ArrayList<>();
|
||||
for (Map.Entry<Class<? extends AlgorithmKeySpec>, AsymEntry<?>> e : asymBuilders.entrySet()) {
|
||||
Object def = null;
|
||||
if (e.getValue().defaultKeySpec != null) {
|
||||
try {
|
||||
def = e.getValue().defaultKeySpec.get();
|
||||
} catch (Throwable t) { // NOPMD
|
||||
def = null;
|
||||
}
|
||||
}
|
||||
out.add(new AsymBuilderInfo(e.getKey(), def));
|
||||
}
|
||||
return Collections.unmodifiableList(out);
|
||||
protected final <S extends AlgorithmKeySpec> void registerPrivateKeyImporter(Class<S> specType,
|
||||
PrivateKeyImporter<S> importer) {
|
||||
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
|
||||
privateKeyImporters.put(specType, Objects.requireNonNull(importer, "importer must not be null"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable descriptor for a symmetric key builder registered with this
|
||||
* algorithm.
|
||||
* Registers symmetric-key generation for one exact specification class.
|
||||
*
|
||||
* <p>
|
||||
* Each {@code SymBuilderInfo} describes the specification type that a
|
||||
* {@link SymmetricKeyBuilder} can handle, along with an optional default
|
||||
* specification object. These descriptors are used for discovery and
|
||||
* documentation purposes, for example when rendering catalog information in
|
||||
* tooling or UIs.
|
||||
* </p>
|
||||
* <p>The optional default is resolved and validated during registration.</p>
|
||||
*
|
||||
* <h2>Usage</h2>
|
||||
* <ul>
|
||||
* <li>Produced by {@link #symmetricBuildersInfo()}.</li>
|
||||
* <li>Displayed to clients for inspection and documentation, but not used
|
||||
* directly in cryptographic operations.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Thread-safety</h2> Being a {@code record}, this type is immutable and
|
||||
* safe to share between threads.
|
||||
*
|
||||
* @param specType the specification type supported by the builder
|
||||
* @param defaultKeySpec an optional default key specification instance, or
|
||||
* {@code null} if no default is provided
|
||||
*/
|
||||
public record SymBuilderInfo(Class<? extends AlgorithmKeySpec> specType, Object defaultKeySpec) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal entry binding a registered symmetric key builder to its optional
|
||||
* default key specification supplier.
|
||||
*
|
||||
* <p>
|
||||
* Each {@code SymEntry} is keyed by a specific {@link AlgorithmKeySpec}
|
||||
* subtype. It holds the {@link SymmetricKeyBuilder} instance capable of
|
||||
* generating or importing keys for that spec, and a supplier that may produce a
|
||||
* default spec when none is provided explicitly.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Usage</h2>
|
||||
* <ul>
|
||||
* <li>Created during calls to
|
||||
* {@link #registerSymmetricKeyBuilder(Class, SymmetricKeyBuilder, Supplier)}.</li>
|
||||
* <li>Looked up internally when methods such as
|
||||
* {@link #generateSecret(AlgorithmKeySpec)} or
|
||||
* {@link #importSecret(AlgorithmKeySpec)} are invoked.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Thread-safety</h2> Immutable and thread-safe by design as a
|
||||
* {@code record}.
|
||||
*
|
||||
* @param builder the builder instance that can create or import keys;
|
||||
* must not be {@code null}
|
||||
* @param defaultKeySpec supplier for a default specification, or {@code null}
|
||||
* if no sensible default exists
|
||||
* @param <S> the type of {@link AlgorithmKeySpec} handled by this
|
||||
* entry
|
||||
*/
|
||||
private record SymEntry<S extends AlgorithmKeySpec>(SymmetricKeyBuilder<S> builder,
|
||||
Supplier<? extends S> defaultKeySpec) {
|
||||
|
||||
/**
|
||||
* Compact constructor that enforces non-null builder.
|
||||
*
|
||||
* @throws NullPointerException if {@code builder} is {@code null}
|
||||
*/
|
||||
SymEntry {
|
||||
Objects.requireNonNull(builder, "builder must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a symmetric key builder for a specific spec type.
|
||||
*
|
||||
* @param specType the spec class accepted by {@code builder}
|
||||
* @param builder builder that can generate/import keys for
|
||||
* {@code specType}
|
||||
* @param defaultKeySpecOrNull optional supplier for a default spec (may be
|
||||
* {@code null})
|
||||
* @param <S> spec type
|
||||
* @throws NullPointerException if {@code specType} or {@code builder} is
|
||||
* @param specType exact specification class
|
||||
* @param generator non-null generator safe for concurrent invocation
|
||||
* @param defaultSpecOrNull optional default supplier, evaluated once
|
||||
* @param <S> specification type
|
||||
* @throws NullPointerException if a required argument or supplied default is
|
||||
* {@code null}
|
||||
* @throws IllegalArgumentException if the supplied default has the wrong type
|
||||
*/
|
||||
protected final <S extends AlgorithmKeySpec> void registerSymmetricKeyBuilder(Class<S> specType,
|
||||
SymmetricKeyBuilder<S> builder, Supplier<? extends S> defaultKeySpecOrNull) {
|
||||
Objects.requireNonNull(specType, "specType must not be null");
|
||||
symBuilders.put(specType, new SymEntry<>(builder, defaultKeySpecOrNull));
|
||||
protected final <S extends AlgorithmKeySpec> void registerSymmetricKeyGenerator(Class<S> specType,
|
||||
SymmetricKeyGenerator<S> generator, Supplier<? extends S> defaultSpecOrNull) {
|
||||
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
|
||||
symmetricKeyGenerators.put(specType, Objects.requireNonNull(generator, "generator must not be null"));
|
||||
symmetricDefaults.put(specType, resolveDefault(specType, defaultSpecOrNull));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the symmetric key builder associated with the given spec type.
|
||||
* Registers symmetric-key import for one exact specification class.
|
||||
*
|
||||
* @param specType spec class used as a lookup key
|
||||
* @param <S> spec type
|
||||
* @return the registered {@link SymmetricKeyBuilder}
|
||||
* @throws IllegalArgumentException if no builder is registered for
|
||||
* {@code specType}
|
||||
* @param specType exact specification class
|
||||
* @param importer non-null importer safe for concurrent invocation
|
||||
* @param <S> specification type
|
||||
* @throws NullPointerException if an argument is {@code null}
|
||||
*/
|
||||
protected final <S extends AlgorithmKeySpec> void registerSymmetricKeyImporter(Class<S> specType,
|
||||
SymmetricKeyImporter<S> importer) {
|
||||
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
|
||||
symmetricKeyImporters.put(specType, Objects.requireNonNull(importer, "importer must not be null"));
|
||||
}
|
||||
|
||||
private IllegalArgumentException missing(String operation, Class<?> specType) {
|
||||
return new IllegalArgumentException(_id + " has no " + operation + " for exact spec " + specType.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the asymmetric key-pair generator registered for an exact
|
||||
* specification class.
|
||||
*
|
||||
* <p>The returned implementation may be shared and invoked concurrently.</p>
|
||||
*
|
||||
* @param specType exact specification class; subclasses are not matched
|
||||
* @param <S> specification type
|
||||
* @return registered generator
|
||||
* @throws NullPointerException if {@code specType} is {@code null}
|
||||
* @throws IllegalArgumentException if no generator is registered
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public final <S extends AlgorithmKeySpec> SymmetricKeyBuilder<S> symmetricKeyBuilder(Class<S> specType) {
|
||||
SymEntry<?> e = symBuilders.get(specType);
|
||||
if (e == null) {
|
||||
throw new IllegalArgumentException(_id + " has no symmetric key builder for " + specType.getName());
|
||||
public final <S extends AlgorithmKeySpec> AsymmetricKeyPairGenerator<S> asymmetricKeyPairGenerator(
|
||||
Class<S> specType) {
|
||||
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
|
||||
AsymmetricKeyPairGenerator<?> generator = keyPairGenerators.get(specType);
|
||||
if (generator == null) {
|
||||
throw missing("asymmetric key-pair generator", specType);
|
||||
}
|
||||
return (SymmetricKeyBuilder<S>) e.builder;
|
||||
return (AsymmetricKeyPairGenerator<S>) generator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns metadata about all registered symmetric builders.
|
||||
* Returns the public-key importer registered for an exact specification class.
|
||||
*
|
||||
* <p>
|
||||
* The default spec value is best-effort; suppliers may throw, in which case
|
||||
* {@code defaultKeySpec} is reported as {@code null}.
|
||||
* </p>
|
||||
* <p>The returned implementation may be shared and invoked concurrently.</p>
|
||||
*
|
||||
* @return immutable list of {@link SymBuilderInfo} descriptors
|
||||
*/
|
||||
public final List<SymBuilderInfo> symmetricBuildersInfo() {
|
||||
List<SymBuilderInfo> out = new ArrayList<>();
|
||||
for (Map.Entry<Class<? extends AlgorithmKeySpec>, SymEntry<?>> e : symBuilders.entrySet()) {
|
||||
Object def = null;
|
||||
if (e.getValue().defaultKeySpec != null) {
|
||||
try {
|
||||
def = e.getValue().defaultKeySpec.get();
|
||||
} catch (Throwable t) { // NOPMD
|
||||
def = null;
|
||||
}
|
||||
}
|
||||
out.add(new SymBuilderInfo(e.getKey(), def));
|
||||
}
|
||||
return Collections.unmodifiableList(out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a fresh symmetric {@link SecretKey} using the registered builder
|
||||
* for {@code spec}.
|
||||
*
|
||||
* @param spec algorithm-specific key specification (must match a registered
|
||||
* symmetric builder)
|
||||
* @param <S> spec type
|
||||
* @return newly generated secret key
|
||||
* @throws NullPointerException if {@code spec} is {@code null}
|
||||
* @throws IllegalArgumentException if no symmetric builder is registered for
|
||||
* {@code spec.getClass()}
|
||||
* @throws GeneralSecurityException if key generation fails or parameters are
|
||||
* unsupported
|
||||
* @param specType exact specification class; subclasses are not matched
|
||||
* @param <S> specification type
|
||||
* @return registered importer
|
||||
* @throws NullPointerException if {@code specType} is {@code null}
|
||||
* @throws IllegalArgumentException if no importer is registered
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public final <S extends AlgorithmKeySpec> SecretKey generateSecret(S spec) throws GeneralSecurityException {
|
||||
Objects.requireNonNull(spec, "spec must not be null");
|
||||
SymmetricKeyBuilder<S> b = symmetricKeyBuilder((Class<S>) spec.getClass());
|
||||
return b.generateSecret(spec);
|
||||
public final <S extends AlgorithmKeySpec> PublicKeyImporter<S> publicKeyImporter(Class<S> specType) {
|
||||
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
|
||||
PublicKeyImporter<?> importer = publicKeyImporters.get(specType);
|
||||
if (importer == null) {
|
||||
throw missing("public-key importer", specType);
|
||||
}
|
||||
return (PublicKeyImporter<S>) importer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports an existing symmetric {@link SecretKey} using the registered builder
|
||||
* for {@code spec}.
|
||||
* Returns the private-key importer registered for an exact specification
|
||||
* class.
|
||||
*
|
||||
* @param spec algorithm-specific key specification including raw
|
||||
* material/format
|
||||
* @param <S> spec type
|
||||
* @return wrapped secret key validated against the spec
|
||||
* @throws NullPointerException if {@code spec} is {@code null}
|
||||
* @throws IllegalArgumentException if no symmetric builder is registered for
|
||||
* {@code spec.getClass()}
|
||||
* @throws GeneralSecurityException if the material is invalid or does not match
|
||||
* the algorithm
|
||||
* <p>The returned implementation may be shared and invoked concurrently.</p>
|
||||
*
|
||||
* @param specType exact specification class; subclasses are not matched
|
||||
* @param <S> specification type
|
||||
* @return registered importer
|
||||
* @throws NullPointerException if {@code specType} is {@code null}
|
||||
* @throws IllegalArgumentException if no importer is registered
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public final <S extends AlgorithmKeySpec> SecretKey importSecret(S spec) throws GeneralSecurityException {
|
||||
Objects.requireNonNull(spec, "spec must not be null");
|
||||
SymmetricKeyBuilder<S> b = symmetricKeyBuilder((Class<S>) spec.getClass());
|
||||
return b.importSecret(spec);
|
||||
public final <S extends AlgorithmKeySpec> PrivateKeyImporter<S> privateKeyImporter(Class<S> specType) {
|
||||
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
|
||||
PrivateKeyImporter<?> importer = privateKeyImporters.get(specType);
|
||||
if (importer == null) {
|
||||
throw missing("private-key importer", specType);
|
||||
}
|
||||
return (PrivateKeyImporter<S>) importer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to generate a {@link KeyPair} using the given asymmetric builder's
|
||||
* default key spec. This method is fully generic and avoids raw types by
|
||||
* capturing the concrete spec type parameter.
|
||||
* Returns the symmetric-key generator registered for an exact specification
|
||||
* class.
|
||||
*
|
||||
* @param specType the spec class label used for diagnostics
|
||||
* @param entry the typed asymmetric builder entry
|
||||
* @param <S> concrete {@link AlgorithmKeySpec} type
|
||||
* @return a freshly generated key pair
|
||||
* @throws GeneralSecurityException if the supplier or builder fails
|
||||
*/
|
||||
private <S extends AlgorithmKeySpec> KeyPair tryGenerateWithDefault(Class<? extends AlgorithmKeySpec> specType,
|
||||
AsymEntry<S> entry) throws GeneralSecurityException {
|
||||
|
||||
if (entry.defaultKeySpec == null) {
|
||||
throw new GeneralSecurityException("no default spec supplier");
|
||||
}
|
||||
|
||||
final S spec;
|
||||
try {
|
||||
spec = entry.defaultKeySpec.get();
|
||||
} catch (Throwable t) { // NOPMD
|
||||
throw new GeneralSecurityException("defaultSpec supplier failed for " + specType.getSimpleName() + ": "
|
||||
+ t.getClass().getSimpleName() + ": " + t.getMessage(), t);
|
||||
}
|
||||
if (spec == null) {
|
||||
throw new GeneralSecurityException("defaultSpec supplier returned null for " + specType.getSimpleName());
|
||||
}
|
||||
|
||||
// No raw types here: S is captured from entry.
|
||||
return entry.builder.generateKeyPair(spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a fresh {@link KeyPair} using the first asymmetric builder that
|
||||
* successfully provides a default key specification.
|
||||
* <p>The returned implementation may be shared and invoked concurrently.</p>
|
||||
*
|
||||
* <p>
|
||||
* This convenience method iterates over all registered asymmetric key builders
|
||||
* that declare a non-null default {@link AlgorithmKeySpec} supplier. For each,
|
||||
* it attempts to obtain the default spec and generate a key pair. If a builder
|
||||
* fails (e.g., the builder only supports import or rejects the parameters), the
|
||||
* method records the failure and continues with the next candidate.
|
||||
* </p>
|
||||
*
|
||||
* <h4>Example</h4> <pre>{@code
|
||||
* CryptoAlgorithm algo = CryptoAlgorithms.require("Ed25519");
|
||||
* KeyPair kp = algo.generateKeyPair();
|
||||
* }</pre>
|
||||
*
|
||||
* @return a newly generated key pair using a default spec from one of the
|
||||
* registered asymmetric builders
|
||||
* @throws IllegalStateException if no builder declares a default spec
|
||||
* supplier
|
||||
* @throws GeneralSecurityException if all candidate builders fail to generate a
|
||||
* key pair; the exception message details
|
||||
* individual causes
|
||||
*/
|
||||
public final KeyPair generateKeyPair() throws GeneralSecurityException {
|
||||
StringBuilder reasons = new StringBuilder(128);
|
||||
boolean attempted = false;
|
||||
|
||||
for (Map.Entry<Class<? extends AlgorithmKeySpec>, AsymEntry<?>> e : asymBuilders.entrySet()) {
|
||||
AsymEntry<?> entry = e.getValue();
|
||||
if (entry.defaultKeySpec == null) {
|
||||
continue;
|
||||
}
|
||||
attempted = true;
|
||||
try {
|
||||
// Wildcard capture lets the compiler infer <S> without casts.
|
||||
return tryGenerateWithDefault(e.getKey(), entry);
|
||||
} catch (GeneralSecurityException ex) {
|
||||
reasons.append(" - ").append(e.getKey().getSimpleName()).append(": ")
|
||||
.append(ex.getClass().getSimpleName()).append(": ").append(String.valueOf(ex.getMessage()))
|
||||
.append('\n');
|
||||
// keep trying other builders
|
||||
}
|
||||
}
|
||||
|
||||
if (!attempted) {
|
||||
throw new IllegalStateException(_id + " has no default asymmetric key spec");
|
||||
}
|
||||
throw new GeneralSecurityException(
|
||||
_id + " failed to generate a default key pair. Reasons:\n" + reasons.toString().trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a fresh {@link KeyPair} using the registered asymmetric builder for
|
||||
* {@code spec}.
|
||||
*
|
||||
* @param spec algorithm-specific key specification (must match a registered
|
||||
* asymmetric builder)
|
||||
* @param <S> spec type
|
||||
* @return newly generated key pair
|
||||
* @throws NullPointerException if {@code spec} is {@code null}
|
||||
* @throws IllegalArgumentException if no asymmetric builder is registered for
|
||||
* {@code spec.getClass()}
|
||||
* @throws GeneralSecurityException if key generation fails or parameters are
|
||||
* unsupported
|
||||
* @param specType exact specification class; subclasses are not matched
|
||||
* @param <S> specification type
|
||||
* @return registered generator
|
||||
* @throws NullPointerException if {@code specType} is {@code null}
|
||||
* @throws IllegalArgumentException if no generator is registered
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public final <S extends AlgorithmKeySpec> KeyPair generateKeyPair(S spec) throws GeneralSecurityException {
|
||||
Objects.requireNonNull(spec, "spec must not be null");
|
||||
AsymmetricKeyBuilder<S> b = asymmetricKeyBuilder((Class<S>) spec.getClass());
|
||||
return b.generateKeyPair(spec);
|
||||
public final <S extends AlgorithmKeySpec> SymmetricKeyGenerator<S> symmetricKeyGenerator(Class<S> specType) {
|
||||
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
|
||||
SymmetricKeyGenerator<?> generator = symmetricKeyGenerators.get(specType);
|
||||
if (generator == null) {
|
||||
throw missing("symmetric-key generator", specType);
|
||||
}
|
||||
return (SymmetricKeyGenerator<S>) generator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a {@link PublicKey} using the registered asymmetric builder for
|
||||
* {@code spec}.
|
||||
* Returns the symmetric-key importer registered for an exact specification
|
||||
* class.
|
||||
*
|
||||
* @param spec algorithm-specific key specification including encoded public
|
||||
* material/format
|
||||
* @param <S> spec type
|
||||
* @return wrapped public key validated against the spec
|
||||
* @throws NullPointerException if {@code spec} is {@code null}
|
||||
* @throws IllegalArgumentException if no asymmetric builder is registered for
|
||||
* {@code spec.getClass()}
|
||||
* @throws GeneralSecurityException if the material is invalid or does not match
|
||||
* the algorithm
|
||||
* <p>The returned implementation may be shared and invoked concurrently.</p>
|
||||
*
|
||||
* @param specType exact specification class; subclasses are not matched
|
||||
* @param <S> specification type
|
||||
* @return registered importer
|
||||
* @throws NullPointerException if {@code specType} is {@code null}
|
||||
* @throws IllegalArgumentException if no importer is registered
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public final <S extends AlgorithmKeySpec> PublicKey importPublic(S spec) throws GeneralSecurityException {
|
||||
Objects.requireNonNull(spec, "spec must not be null");
|
||||
AsymmetricKeyBuilder<S> b = asymmetricKeyBuilder((Class<S>) spec.getClass());
|
||||
return b.importPublic(spec);
|
||||
public final <S extends AlgorithmKeySpec> SymmetricKeyImporter<S> symmetricKeyImporter(Class<S> specType) {
|
||||
Objects.requireNonNull(specType, SPEC_TYPE_NULL);
|
||||
SymmetricKeyImporter<?> importer = symmetricKeyImporters.get(specType);
|
||||
if (importer == null) {
|
||||
throw missing("symmetric-key importer", specType);
|
||||
}
|
||||
return (SymmetricKeyImporter<S>) importer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a {@link PrivateKey} using the registered asymmetric builder for
|
||||
* {@code spec}.
|
||||
* Returns deterministic metadata for every exact key operation.
|
||||
*
|
||||
* @param spec algorithm-specific key specification including encoded private
|
||||
* material/format
|
||||
* @param <S> spec type
|
||||
* @return wrapped private key validated against the spec
|
||||
* @throws NullPointerException if {@code spec} is {@code null}
|
||||
* @throws IllegalArgumentException if no asymmetric builder is registered for
|
||||
* {@code spec.getClass()}
|
||||
* @throws GeneralSecurityException if the material is invalid or does not match
|
||||
* the algorithm
|
||||
* @return immutable metadata ordered by operation and specification class
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public final <S extends AlgorithmKeySpec> PrivateKey importPrivate(S spec) throws GeneralSecurityException {
|
||||
Objects.requireNonNull(spec, "spec must not be null");
|
||||
AsymmetricKeyBuilder<S> b = asymmetricKeyBuilder((Class<S>) spec.getClass());
|
||||
return b.importPrivate(spec);
|
||||
public final List<KeyOperationInfo> keyOperations() {
|
||||
List<KeyOperationInfo> result = new ArrayList<>();
|
||||
addOperationInfo(result, KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE, keyPairGenerators,
|
||||
asymmetricDefaults);
|
||||
addOperationInfo(result, KeyOperation.ASYMMETRIC_PUBLIC_IMPORT, publicKeyImporters, Map.of());
|
||||
addOperationInfo(result, KeyOperation.ASYMMETRIC_PRIVATE_IMPORT, privateKeyImporters, Map.of());
|
||||
addOperationInfo(result, KeyOperation.SYMMETRIC_GENERATE, symmetricKeyGenerators, symmetricDefaults);
|
||||
addOperationInfo(result, KeyOperation.SYMMETRIC_IMPORT, symmetricKeyImporters, Map.of());
|
||||
result.sort(Comparator.comparing(KeyOperationInfo::operation)
|
||||
.thenComparing(info -> info.specType().getName()));
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private static void addOperationInfo(List<KeyOperationInfo> result, KeyOperation operation,
|
||||
Map<Class<? extends AlgorithmKeySpec>, ?> operations,
|
||||
Map<Class<? extends AlgorithmKeySpec>, AlgorithmKeySpec> defaults) {
|
||||
for (Class<? extends AlgorithmKeySpec> specType : operations.keySet()) {
|
||||
result.add(new KeyOperationInfo(operation, specType, defaults.get(specType)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,615 +1,72 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
* are permitted provided that the conditions in the project LICENSE are met.
|
||||
******************************************************************************/
|
||||
package zeroecho.core;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import zeroecho.core.audit.AuditListener;
|
||||
import zeroecho.core.audit.AuditedContexts;
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
import zeroecho.core.context.CryptoContext;
|
||||
import zeroecho.core.context.DigestContext;
|
||||
import zeroecho.core.context.EncryptionContext;
|
||||
import zeroecho.core.context.KemContext;
|
||||
import zeroecho.core.context.MacContext;
|
||||
import zeroecho.core.context.SignatureContext;
|
||||
import zeroecho.core.err.UnsupportedRoleException;
|
||||
import zeroecho.core.err.UnsupportedSpecException;
|
||||
import zeroecho.core.policy.CryptoPolicy;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* Static façade and registry for {@link CryptoAlgorithm} providers.
|
||||
* Immutable registry of {@link CryptoAlgorithm} providers.
|
||||
*
|
||||
* <p>
|
||||
* {@code CryptoAlgorithms} discovers algorithms via {@link ServiceLoader} and
|
||||
* exposes:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>a registry from canonical algorithm id to implementation,</li>
|
||||
* <li>policy hooks that validate requested operations before contexts are
|
||||
* created,</li>
|
||||
* <li>global audit wiring (listener + wrapping mode), and</li>
|
||||
* <li>convenience methods for context creation and key generation/import.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Discovery & identity</h2> Implementations register themselves using
|
||||
* the Java SPI for {@link CryptoAlgorithm}. If multiple providers advertise the
|
||||
* same {@linkplain CryptoAlgorithm#id() id}, the registry throws at startup to
|
||||
* avoid ambiguous resolution.
|
||||
*
|
||||
* <h2>Policy</h2> The active {@link CryptoPolicy} is consulted before any
|
||||
* context is created. Policies can deny weak parameters, enforce key-usage
|
||||
* separation, or restrict algorithms. If {@link #setPolicy(CryptoPolicy)} is
|
||||
* never called or is set to {@code null}, a permissive policy is used.
|
||||
*
|
||||
* <h2>Auditing</h2> All key lifecycle events and context creation can be
|
||||
* reported to a global {@link AuditListener}. The {@link AuditMode} determines
|
||||
* whether contexts are wrapped with auditing proxies or relied upon to emit
|
||||
* events directly.
|
||||
*
|
||||
* <h2>Thread-safety</h2> The registry map and global hooks are safe to read
|
||||
* concurrently. Hooks are backed by {@code volatile} fields and can be swapped
|
||||
* at runtime; there is no global lock.
|
||||
* <p>Providers are discovered once through {@link ServiceLoader}, sorted by
|
||||
* canonical algorithm identifier, and retained in one immutable registry.
|
||||
* Runtime policy and auditing belong exclusively to explicitly created
|
||||
* {@link zeroecho.sdk.ZeroEchoSession} instances.</p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class CryptoAlgorithms {
|
||||
|
||||
private static final Map<String, CryptoAlgorithm> BY_ID;
|
||||
private static volatile CryptoPolicy<ContextSpec, Key> POLICY = CryptoPolicy.permissive(); // NOPMD
|
||||
private static volatile AuditListener AUDIT = AuditListener.noop(); // NOPMD
|
||||
private static volatile AuditMode AUDIT_MODE = AuditMode.OFF; // NOPMD
|
||||
private static final Map<String, CryptoAlgorithm> BY_ID = loadRegistry();
|
||||
|
||||
private CryptoAlgorithms() {
|
||||
}
|
||||
|
||||
static {
|
||||
Map<String, CryptoAlgorithm> m = new HashMap<>();
|
||||
for (CryptoAlgorithm a : ServiceLoader.load(CryptoAlgorithm.class)) {
|
||||
CryptoAlgorithm prev = m.put(a.id(), a);
|
||||
if (prev != null) {
|
||||
throw new IllegalStateException("Duplicate algorithm id: " + a.id());
|
||||
private static Map<String, CryptoAlgorithm> loadRegistry() {
|
||||
Map<String, CryptoAlgorithm> algorithms = new TreeMap<>();
|
||||
for (CryptoAlgorithm algorithm : ServiceLoader.load(CryptoAlgorithm.class)) {
|
||||
CryptoAlgorithm previous = algorithms.put(algorithm.id(), algorithm);
|
||||
if (previous != null) {
|
||||
throw new IllegalStateException("Duplicate algorithm id: " + algorithm.id());
|
||||
}
|
||||
}
|
||||
BY_ID = Collections.unmodifiableMap(m);
|
||||
return Collections.unmodifiableMap(new LinkedHashMap<>(algorithms));
|
||||
}
|
||||
|
||||
/* default */ static Map<String, CryptoAlgorithm> registry() {
|
||||
return BY_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of available algorithm identifiers discovered via
|
||||
* {@link ServiceLoader}.
|
||||
* Returns registered algorithm identifiers in deterministic order.
|
||||
*
|
||||
* <p>
|
||||
* The returned set is backed by an unmodifiable registry snapshot. Use these
|
||||
* identifiers with {@link #require(String)} or the convenience methods below.
|
||||
* </p>
|
||||
*
|
||||
* @return unmodifiable set of canonical algorithm ids
|
||||
* @return unmodifiable set of canonical identifiers
|
||||
*/
|
||||
public static Set<String> available() {
|
||||
return BY_ID.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up an algorithm implementation by its canonical identifier.
|
||||
* Resolves an algorithm by canonical identifier.
|
||||
*
|
||||
* <p>
|
||||
* If the id is unknown, an {@link IllegalArgumentException} is thrown. This
|
||||
* method is preferred over direct access to ensure consistent error handling
|
||||
* and to centralize future selection logic.
|
||||
* </p>
|
||||
*
|
||||
* @param id canonical algorithm identifier (e.g., {@code "AES/GCM"} or
|
||||
* {@code "Ed25519"})
|
||||
* @return the corresponding {@link CryptoAlgorithm} implementation
|
||||
* @throws IllegalArgumentException if no algorithm is registered under
|
||||
* @param id canonical algorithm identifier
|
||||
* @return registered algorithm
|
||||
* @throws IllegalArgumentException if no algorithm is registered with
|
||||
* {@code id}
|
||||
*/
|
||||
public static CryptoAlgorithm require(String id) {
|
||||
CryptoAlgorithm a = BY_ID.get(id);
|
||||
if (a == null) {
|
||||
CryptoAlgorithm algorithm = BY_ID.get(id);
|
||||
if (algorithm == null) {
|
||||
throw new IllegalArgumentException("Unknown algorithm id: " + id);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the global cryptographic policy applied before any context creation.
|
||||
*
|
||||
* <p>
|
||||
* Pass {@code null} to revert to a permissive policy. Policies should be fast
|
||||
* and side-effect free; they are invoked on every
|
||||
* {@link #create(String, KeyUsage, Key, ContextSpec)} call.
|
||||
* </p>
|
||||
*
|
||||
* @param p policy to install, or {@code null} to use
|
||||
* {@link CryptoPolicy#permissive()}
|
||||
*/
|
||||
public static void setPolicy(CryptoPolicy<ContextSpec, Key> p) {
|
||||
POLICY = (p == null ? CryptoPolicy.<ContextSpec, Key>permissive() : p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the global {@link AuditListener}.
|
||||
*
|
||||
* <p>
|
||||
* Pass {@code null} to disable custom auditing (a no-op listener will be
|
||||
* installed). The listener may be invoked by context proxies (in
|
||||
* {@link AuditMode#WRAP}) and by the convenience key factory methods below.
|
||||
* </p>
|
||||
*
|
||||
* @param l listener instance or {@code null} for a no-op listener
|
||||
*/
|
||||
public static void setAuditListener(AuditListener l) {
|
||||
AUDIT = (l == null ? AuditListener.noop() : l);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current global {@link AuditListener}.
|
||||
*
|
||||
* @return the active audit listener (never {@code null})
|
||||
*/
|
||||
public static AuditListener audit() {
|
||||
return AUDIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declares how auditing is applied to cryptographic contexts.
|
||||
*
|
||||
* <p>
|
||||
* The {@code AuditMode} controls whether contexts created by
|
||||
* {@link CryptoAlgorithms#create(String, KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}
|
||||
* are wrapped in auditing proxies or whether auditing is delegated entirely to
|
||||
* the caller.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Modes</h2>
|
||||
* <ul>
|
||||
* <li>{@link #OFF} - No automatic wrapping of contexts (default). Only explicit
|
||||
* events triggered at creation are emitted; no per-operation auditing is
|
||||
* injected.</li>
|
||||
*
|
||||
* <li>{@link #WRAP} - Supported contexts are wrapped in dynamic proxies that
|
||||
* emit additional stream-level and per-operation auditing events. Creation
|
||||
* events originate from the proxy rather than the factory method.</li>
|
||||
*
|
||||
* <li>{@link #MANUAL} - No automatic wrapping and no automatic event emission.
|
||||
* The caller is fully responsible for invoking audit methods (e.g.,
|
||||
* {@link CryptoAlgorithms#audit()}) at the appropriate times.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public enum AuditMode {
|
||||
/**
|
||||
* No automatic wrapping of contexts (default).
|
||||
*
|
||||
* <p>
|
||||
* Only explicit events emitted here (e.g.,
|
||||
* {@link AuditListener#onContextCreated}) are sent to the listener;
|
||||
* stream-level or per-operation auditing is not injected.
|
||||
* </p>
|
||||
*/
|
||||
OFF,
|
||||
/**
|
||||
* Wraps supported contexts in dynamic proxies that emit stream-level auditing.
|
||||
*
|
||||
* <p>
|
||||
* In this mode, creation events are emitted by the proxy rather than here, and
|
||||
* subsequent operations (e.g., updates, finalization) may also be audited
|
||||
* depending on the proxy implementation.
|
||||
* </p>
|
||||
*/
|
||||
WRAP,
|
||||
/**
|
||||
* No wrapping and no automatic events.
|
||||
*
|
||||
* <p>
|
||||
* The caller is responsible for emitting all relevant audit events via the
|
||||
* {@link #audit()} listener.
|
||||
* </p>
|
||||
*/
|
||||
MANUAL
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the auditing mode for subsequently created contexts.
|
||||
*
|
||||
* <p>
|
||||
* Passing {@code null} resets the mode to {@link AuditMode#OFF}.
|
||||
* </p>
|
||||
*
|
||||
* @param mode desired auditing strategy or {@code null} for {@code OFF}
|
||||
*/
|
||||
public static void setAuditMode(AuditMode mode) {
|
||||
AUDIT_MODE = (mode == null ? AuditMode.OFF : mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current auditing mode.
|
||||
*
|
||||
* @return active {@link AuditMode}; never {@code null}
|
||||
*/
|
||||
public static AuditMode getAuditMode() {
|
||||
return AUDIT_MODE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link CryptoContext} for the given algorithm id and role, applying
|
||||
* policy validation and optional auditing/wrapping.
|
||||
*
|
||||
* <p>
|
||||
* Flow:
|
||||
* </p>
|
||||
* <ol>
|
||||
* <li>Policy validation via
|
||||
* {@link CryptoPolicy#validate(String, KeyUsage, Key, ContextSpec)}.</li>
|
||||
* <li>Algorithm resolution via {@link #require(String)} and context
|
||||
* construction via
|
||||
* {@link CryptoAlgorithm#create(KeyUsage, Key, ContextSpec)}.</li>
|
||||
* <li>Auditing behavior based on {@link #getAuditMode()}:
|
||||
* <ul>
|
||||
* <li>{@link AuditMode#OFF}/{@link AuditMode#MANUAL}: emit a creation event
|
||||
* immediately via
|
||||
* {@link AuditListener#onContextCreated(String, String, KeyUsage, Key, ContextSpec)}.</li>
|
||||
* <li>{@link AuditMode#WRAP}: return a proxy (where supported) that emits
|
||||
* creation and stream-level events; unknown context types are returned
|
||||
* unwrapped.</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* @param id canonical algorithm identifier
|
||||
* @param role desired {@link KeyUsage} (e.g., ENCRYPT, VERIFY)
|
||||
* @param key key instance for the role
|
||||
* @param spec optional context specification; may be {@code null} to use
|
||||
* algorithm defaults
|
||||
* @param <C> context type
|
||||
* @param <K> key type
|
||||
* @param <S> spec type
|
||||
* @return a context ready for use; may be a proxy if {@link AuditMode#WRAP} is
|
||||
* active
|
||||
* @throws IOException if the underlying algorithm fails to create
|
||||
* a context
|
||||
* @throws IllegalArgumentException if {@code id} is unknown
|
||||
* @throws UnsupportedRoleException if the algorithm does not support
|
||||
* {@code role}
|
||||
* @throws UnsupportedSpecException if the provided key/spec are incompatible
|
||||
* with the role
|
||||
*/
|
||||
public static <C extends CryptoContext, K extends Key, S extends ContextSpec> C create(String id, KeyUsage role,
|
||||
K key, S spec) throws IOException {
|
||||
|
||||
POLICY.validate(id, role, key, spec);
|
||||
|
||||
CryptoAlgorithm algo = require(id);
|
||||
C ctx = algo.create(role, key, spec);
|
||||
|
||||
// In WRAP mode, the proxy will emit creation metadata/events.
|
||||
if (AUDIT_MODE != AuditMode.WRAP) {
|
||||
AUDIT.onContextCreated(algo.id(), algo.providerName(), role, key, spec);
|
||||
}
|
||||
|
||||
if (AUDIT_MODE == AuditMode.WRAP) {
|
||||
final AuditListener listener = AUDIT; // pass through the global listener
|
||||
return switch (ctx) {
|
||||
case SignatureContext signatureContext -> wrapForAudit(signatureContext, listener, role);
|
||||
case EncryptionContext encryptionContext -> wrapForAudit(encryptionContext, listener, role);
|
||||
case KemContext kemContext -> wrapForAudit(kemContext, listener, role);
|
||||
case DigestContext digestContext -> wrapForAudit(digestContext, listener, role);
|
||||
case MacContext macContext -> wrapForAudit(macContext, listener, role);
|
||||
case AgreementContext agreementContext -> wrapForAudit(agreementContext, listener, role);
|
||||
};
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the audited wrapper for the supplied context.
|
||||
*
|
||||
* <p>
|
||||
* The returned context remains owned by the caller of the factory method. This
|
||||
* helper does not acquire an additional resource requiring local cleanup.
|
||||
* </p>
|
||||
*
|
||||
* @param <C> context type
|
||||
* @param context source context
|
||||
* @param listener audit listener
|
||||
* @param role key usage role
|
||||
* @return audited wrapper
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
/* default */ static <C extends CryptoContext> C wrapForAudit(CryptoContext context, AuditListener listener,
|
||||
KeyUsage role) {
|
||||
return (C) AuditedContexts.wrap(context, listener, role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link CryptoContext} using the algorithm’s 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 algorithm’s 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 algorithm’s 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 algorithm’s registered
|
||||
* builder.
|
||||
*
|
||||
* <p>
|
||||
* Emits {@link AuditListener#onKeyBuilt(String, String, AlgorithmKeySpec, Key)}
|
||||
* on success.
|
||||
* </p>
|
||||
*
|
||||
* @param id canonical algorithm identifier
|
||||
* @param spec algorithm-specific key specification containing raw/encoded
|
||||
* material
|
||||
* @param <S> spec type
|
||||
* @return imported secret key
|
||||
* @throws GeneralSecurityException if import fails or material is invalid
|
||||
* @throws IllegalArgumentException if {@code id} is unknown or the spec is
|
||||
* unsupported
|
||||
*/
|
||||
public static <S extends AlgorithmKeySpec> SecretKey secretKey(String id, S spec) throws GeneralSecurityException {
|
||||
CryptoAlgorithm algo = require(id);
|
||||
@SuppressWarnings("unchecked")
|
||||
SecretKey k = algo.symmetricKeyBuilder((Class<S>) spec.getClass()).importSecret(spec);
|
||||
AUDIT.onKeyBuilt(algo.id(), algo.providerName(), spec, k);
|
||||
return k;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to destroy a key via the JDK {@code Destroyable} interface.
|
||||
*
|
||||
* <p>
|
||||
* If destruction succeeds,
|
||||
* {@link AuditListener#onKeyDestroyed(String, String, Key)} is emitted. Any
|
||||
* exceptions from {@code destroy()} are swallowed; the method returns
|
||||
* {@code false} when destruction did not occur.
|
||||
* </p>
|
||||
*
|
||||
* @param algoId algorithm identifier used for audit metadata
|
||||
* @param provider provider name used for audit metadata
|
||||
* @param key key to destroy
|
||||
* @return {@code true} if the key reported destroyed, {@code false} otherwise
|
||||
*/
|
||||
public static boolean destroyKey(String algoId, String provider, Key key) {
|
||||
boolean destroyed = false;
|
||||
try {
|
||||
if (key instanceof javax.security.auth.Destroyable) {
|
||||
javax.security.auth.Destroyable d = (javax.security.auth.Destroyable) key;
|
||||
if (!d.isDestroyed()) {
|
||||
d.destroy();
|
||||
destroyed = true;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// swallow and report via audit only if destroyed
|
||||
}
|
||||
if (destroyed) {
|
||||
AUDIT.onKeyDestroyed(algoId, provider, key);
|
||||
}
|
||||
return destroyed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrapper for
|
||||
* {@link CryptoAlgorithm#generateSecret(AlgorithmKeySpec)}.
|
||||
*
|
||||
* @param id canonical algorithm identifier
|
||||
* @param spec algorithm-specific key specification
|
||||
* @param <S> spec type
|
||||
* @return newly generated secret key
|
||||
* @throws GeneralSecurityException if key generation fails
|
||||
* @throws IllegalArgumentException if {@code id} is unknown
|
||||
*/
|
||||
public static <S extends AlgorithmKeySpec> SecretKey generateSecret(String id, S spec)
|
||||
throws GeneralSecurityException {
|
||||
return require(id).generateSecret(spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrapper for
|
||||
* {@link CryptoAlgorithm#generateKeyPair(AlgorithmKeySpec)}.
|
||||
*
|
||||
* @param id canonical algorithm identifier
|
||||
* @param spec algorithm-specific key specification
|
||||
* @param <S> spec type
|
||||
* @return newly generated key pair
|
||||
* @throws GeneralSecurityException if key generation fails
|
||||
* @throws IllegalArgumentException if {@code id} is unknown
|
||||
*/
|
||||
public static <S extends AlgorithmKeySpec> KeyPair generateKeyPair(String id, S spec)
|
||||
throws GeneralSecurityException {
|
||||
return require(id).generateKeyPair(spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrapper for
|
||||
* {@link CryptoAlgorithm#importPublic(AlgorithmKeySpec)}.
|
||||
*
|
||||
* @param id canonical algorithm identifier
|
||||
* @param spec algorithm-specific key specification
|
||||
* @param <S> spec type
|
||||
* @return imported public key
|
||||
* @throws GeneralSecurityException if import fails
|
||||
* @throws IllegalArgumentException if {@code id} is unknown
|
||||
*/
|
||||
public static <S extends AlgorithmKeySpec> PublicKey importPublic(String id, S spec)
|
||||
throws GeneralSecurityException {
|
||||
return require(id).importPublic(spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrapper for
|
||||
* {@link CryptoAlgorithm#importPrivate(AlgorithmKeySpec)}.
|
||||
*
|
||||
* @param id canonical algorithm identifier
|
||||
* @param spec algorithm-specific key specification
|
||||
* @param <S> spec type
|
||||
* @return imported private key
|
||||
* @throws GeneralSecurityException if import fails
|
||||
* @throws IllegalArgumentException if {@code id} is unknown
|
||||
*/
|
||||
public static <S extends AlgorithmKeySpec> PrivateKey importPrivate(String id, S spec)
|
||||
throws GeneralSecurityException {
|
||||
return require(id).importPrivate(spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrapper for
|
||||
* {@link CryptoAlgorithm#importSecret(AlgorithmKeySpec)}.
|
||||
*
|
||||
* @param id canonical algorithm identifier
|
||||
* @param spec algorithm-specific key specification
|
||||
* @param <S> spec type
|
||||
* @return imported secret key
|
||||
* @throws GeneralSecurityException if import fails
|
||||
* @throws IllegalArgumentException if {@code id} is unknown
|
||||
*/
|
||||
public static <S extends AlgorithmKeySpec> SecretKey importSecret(String id, S spec)
|
||||
throws GeneralSecurityException {
|
||||
return require(id).importSecret(spec);
|
||||
return algorithm;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,7 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.ServiceLoader;
|
||||
|
||||
import zeroecho.core.annotation.Describable;
|
||||
import zeroecho.core.annotation.DisplayName;
|
||||
@@ -47,8 +44,8 @@ import zeroecho.core.annotation.DisplayName;
|
||||
*
|
||||
* <p>
|
||||
* {@code CryptoCatalog} is a lightweight registry built at a point in time via
|
||||
* {@link #load()}. It collects algorithms published through the Java SPI for
|
||||
* {@link CryptoAlgorithm}, ensures identifier uniqueness, and exposes:
|
||||
* {@link #load()}. It consumes the authoritative provider registry owned by
|
||||
* {@link CryptoAlgorithms} and exposes:
|
||||
* </p>
|
||||
*
|
||||
* <ul>
|
||||
@@ -60,8 +57,8 @@ import zeroecho.core.annotation.DisplayName;
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Identity and uniqueness</h2> Algorithm ids are treated as canonical keys.
|
||||
* If two providers expose the same {@linkplain CryptoAlgorithm#id() id}, the
|
||||
* catalog build fails with {@link IllegalStateException}.
|
||||
* Duplicate provider identifiers are rejected when the authoritative registry
|
||||
* is initialized.
|
||||
*
|
||||
* <h2>Immutability & thread-safety</h2> After construction, the internal
|
||||
* map is unmodifiable and safe to share across threads. This class performs no
|
||||
@@ -77,10 +74,9 @@ import zeroecho.core.annotation.DisplayName;
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* <b>Note:</b> Default spec / key-spec values shown in outputs are derived from
|
||||
* {@code Supplier}s registered by algorithms. Suppliers may compute labels or
|
||||
* return lightweight descriptors; their intent is documentation, not
|
||||
* round‑tripping.
|
||||
* <b>Note:</b> Default spec / key-spec values shown in outputs are stable
|
||||
* metadata values resolved when providers are initialized; their intent is
|
||||
* documentation, not round-tripping.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
@@ -93,26 +89,25 @@ public final class CryptoCatalog {
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers {@link CryptoAlgorithm} implementations via {@link ServiceLoader}
|
||||
* and returns an immutable catalog snapshot.
|
||||
* Returns a catalog view of the authoritative registry initialized by
|
||||
* {@link CryptoAlgorithms}.
|
||||
*
|
||||
* <p>
|
||||
* During loading, algorithm ids are checked for uniqueness. A duplicate id
|
||||
* results in an {@link IllegalStateException} to prevent ambiguous resolution.
|
||||
* Provider discovery, deterministic ordering, and duplicate checking occur
|
||||
* once in {@code CryptoAlgorithms}. This method neither scans providers nor
|
||||
* copies their collection.
|
||||
* </p>
|
||||
*
|
||||
* @return an immutable {@code CryptoCatalog} with all discovered algorithms
|
||||
* @throws IllegalStateException if two providers declare the same algorithm id
|
||||
* @throws ExceptionInInitializerError if authoritative provider initialization
|
||||
* fails
|
||||
*/
|
||||
public static CryptoCatalog load() {
|
||||
Map<String, CryptoAlgorithm> m = new HashMap<>();
|
||||
ServiceLoader.load(CryptoAlgorithm.class).forEach(a -> {
|
||||
CryptoAlgorithm prev = m.put(a.id(), a);
|
||||
if (prev != null) {
|
||||
throw new IllegalStateException("Duplicate algorithm id: " + a.id());
|
||||
}
|
||||
});
|
||||
return new CryptoCatalog(Collections.unmodifiableMap(m));
|
||||
return new CryptoCatalog(CryptoAlgorithms.registry());
|
||||
}
|
||||
|
||||
/* default */ Map<String, CryptoAlgorithm> algorithms() {
|
||||
return algos;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,9 +127,8 @@ public final class CryptoCatalog {
|
||||
StringBuilder sb = null;
|
||||
for (CryptoAlgorithm a : algos.values()) {
|
||||
boolean hasCaps = !a.listCapabilities().isEmpty();
|
||||
boolean hasAsym = !a.asymmetricBuildersInfo().isEmpty();
|
||||
boolean hasSym = !a.symmetricBuildersInfo().isEmpty();
|
||||
if (!hasCaps && !hasAsym && !hasSym) {
|
||||
boolean hasKeyOperations = !a.keyOperations().isEmpty();
|
||||
if (!hasCaps && !hasKeyOperations) {
|
||||
if (sb == null) {
|
||||
sb = new StringBuilder(50 /* minimal record size */ * 6 /* suggested avg of error records */); // NOPMD
|
||||
}
|
||||
@@ -225,30 +219,20 @@ public final class CryptoCatalog {
|
||||
.append(jsonField("contextType", cap.contextType().getSimpleName())).append(',')
|
||||
.append(jsonField("keyType", cap.keyType().getSimpleName())).append(',')
|
||||
.append(jsonField("specType", cap.specType().getSimpleName())).append(",\"defaultSpec\":")
|
||||
.append(cap.defaultSpec() == null ? "null" : jsonString(labelOf(cap.defaultSpec().get())))
|
||||
.append(cap.defaultSpec() == null ? "null" : jsonString(labelOf(cap.defaultSpec())))
|
||||
.append('}');
|
||||
}
|
||||
sb.append("],\"asymmetricKeyBuilders\":[");
|
||||
boolean fa = true;
|
||||
for (CryptoAlgorithm.AsymBuilderInfo kb : a.asymmetricBuildersInfo()) {
|
||||
if (!fa) {
|
||||
sb.append("],\"keyOperations\":[");
|
||||
boolean firstOperation = true;
|
||||
for (KeyOperationInfo operation : a.keyOperations()) {
|
||||
if (!firstOperation) {
|
||||
sb.append(',');
|
||||
}
|
||||
fa = false;
|
||||
sb.append('{').append(jsonField("specType", kb.specType.getSimpleName())).append(",\"defaultKeySpec\":")
|
||||
.append(kb.defaultKeySpec == null ? "null" : jsonString(labelOf(kb.defaultKeySpec)))
|
||||
.append('}');
|
||||
}
|
||||
sb.append("],\"symmetricKeyBuilders\":[");
|
||||
boolean fs = true;
|
||||
for (CryptoAlgorithm.SymBuilderInfo kb : a.symmetricBuildersInfo()) {
|
||||
if (!fs) {
|
||||
sb.append(',');
|
||||
}
|
||||
fs = false;
|
||||
sb.append('{').append(jsonField("specType", kb.specType().getSimpleName()))
|
||||
.append(",\"defaultKeySpec\":")
|
||||
.append(kb.defaultKeySpec() == null ? "null" : jsonString(labelOf(kb.defaultKeySpec())))
|
||||
firstOperation = false;
|
||||
sb.append('{').append(jsonField("operation", operation.operation().name())).append(',')
|
||||
.append(jsonField("specType", operation.specType().getSimpleName()))
|
||||
.append(",\"defaultSpec\":")
|
||||
.append(operation.defaultSpec() == null ? "null" : jsonString(labelOf(operation.defaultSpec())))
|
||||
.append('}');
|
||||
}
|
||||
sb.append("]}");
|
||||
@@ -285,23 +269,17 @@ public final class CryptoCatalog {
|
||||
.append(esc(cap.contextType().getSimpleName())).append("</contextType><keyType>")
|
||||
.append(esc(cap.keyType().getSimpleName())).append("</keyType><specType>")
|
||||
.append(esc(cap.specType().getSimpleName())).append("</specType><defaultSpec>")
|
||||
.append(esc(labelOf(cap.defaultSpec().get()))).append("</defaultSpec></capability>");
|
||||
.append(esc(labelOf(cap.defaultSpec()))).append("</defaultSpec></capability>");
|
||||
}
|
||||
sb.append("</capabilities><asymmetricKeyBuilders>");
|
||||
for (CryptoAlgorithm.AsymBuilderInfo kb : a.asymmetricBuildersInfo()) {
|
||||
sb.append("<keyBuilder specType=\"").append(esc(kb.specType.getSimpleName()))
|
||||
.append("\"><defaultKeySpec>")
|
||||
.append(kb.defaultKeySpec == null ? "" : esc(labelOf(kb.defaultKeySpec)))
|
||||
.append("</defaultKeySpec></keyBuilder>");
|
||||
sb.append("</capabilities><keyOperations>");
|
||||
for (KeyOperationInfo operation : a.keyOperations()) {
|
||||
sb.append("<keyOperation operation=\"").append(operation.operation().name())
|
||||
.append("\" specType=\"").append(esc(operation.specType().getSimpleName()))
|
||||
.append("\"><defaultSpec>")
|
||||
.append(operation.defaultSpec() == null ? "" : esc(labelOf(operation.defaultSpec())))
|
||||
.append("</defaultSpec></keyOperation>");
|
||||
}
|
||||
sb.append("</asymmetricKeyBuilders><symmetricKeyBuilders>");
|
||||
for (CryptoAlgorithm.SymBuilderInfo kb : a.symmetricBuildersInfo()) {
|
||||
sb.append("<keyBuilder specType=\"").append(esc(kb.specType().getSimpleName()))
|
||||
.append("\"><defaultKeySpec>")
|
||||
.append(kb.defaultKeySpec() == null ? "" : esc(labelOf(kb.defaultKeySpec())))
|
||||
.append("</defaultKeySpec></keyBuilder>");
|
||||
}
|
||||
sb.append("</symmetricKeyBuilders></algorithm>");
|
||||
sb.append("</keyOperations></algorithm>");
|
||||
}
|
||||
sb.append("</cryptoCatalog>");
|
||||
return sb.toString();
|
||||
|
||||
26
lib/src/main/java/zeroecho/core/KeyOperation.java
Normal file
26
lib/src/main/java/zeroecho/core/KeyOperation.java
Normal 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
|
||||
}
|
||||
47
lib/src/main/java/zeroecho/core/KeyOperationInfo.java
Normal file
47
lib/src/main/java/zeroecho/core/KeyOperationInfo.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
package zeroecho.core.alg;
|
||||
|
||||
import java.security.Key;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import zeroecho.core.AlgorithmFamily;
|
||||
@@ -42,7 +43,7 @@ import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.context.CryptoContext;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
import zeroecho.core.spi.ContextConstructorKS;
|
||||
import zeroecho.core.spi.ContextFactoryKS;
|
||||
|
||||
/**
|
||||
* Convenience base class for concrete {@link CryptoAlgorithm} implementations.
|
||||
@@ -54,7 +55,7 @@ import zeroecho.core.spi.ContextConstructorKS;
|
||||
*
|
||||
* <ol>
|
||||
* <li><b>Binding roles to runtime factories</b> via
|
||||
* {@link #capability(AlgorithmFamily, KeyUsage, Class, Class, Class, ContextConstructorKS, Supplier)},
|
||||
* {@link #capability(AlgorithmFamily, KeyUsage, Class, Class, Class, ContextFactoryKS, Supplier)},
|
||||
* which registers a {@link KeyUsage role} together with its expected
|
||||
* {@link CryptoContext} type, accepted {@link Key} type, optional
|
||||
* {@link ContextSpec} type, the constructor factory, and a default spec
|
||||
@@ -134,8 +135,8 @@ public abstract class AbstractCryptoAlgorithm extends CryptoAlgorithm {
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li><b>Runtime binding:</b> delegates to
|
||||
* {@link CryptoAlgorithm#bind(KeyUsage, Class, Class, Class, ContextConstructorKS, Supplier)}
|
||||
* so that {@link CryptoAlgorithm#create(KeyUsage, Key, ContextSpec)} can
|
||||
* {@link CryptoAlgorithm#bindContext(KeyUsage, Class, Class, Class, ContextFactoryKS, Supplier)}
|
||||
* so that {@link CryptoAlgorithm#createContext(KeyUsage, Key, ContextSpec)} can
|
||||
* construct the appropriate {@link CryptoContext} when invoked.</li>
|
||||
* <li><b>Metadata publication:</b> creates a {@link Capability} describing this
|
||||
* role (algorithm id, {@link AlgorithmFamily family}, role, context/key/spec
|
||||
@@ -145,7 +146,8 @@ public abstract class AbstractCryptoAlgorithm extends CryptoAlgorithm {
|
||||
* </ul>
|
||||
*
|
||||
* <h4>Validation</h4> Type checks happen at creation time (via {@code bind})
|
||||
* and again when {@link CryptoAlgorithm#create(KeyUsage, Key, ContextSpec)} is
|
||||
* and again when
|
||||
* {@link CryptoAlgorithm#createContext(KeyUsage, Key, ContextSpec)} is
|
||||
* called. If a factory returns a context not assignable to {@code ctxType}, an
|
||||
* {@link IllegalStateException} will be thrown.
|
||||
*
|
||||
@@ -157,21 +159,24 @@ public abstract class AbstractCryptoAlgorithm extends CryptoAlgorithm {
|
||||
* @param keyType accepted {@link Key} type for this role
|
||||
* @param specType accepted {@link ContextSpec} type (may be a marker type)
|
||||
* @param factory constructor that builds a context for (key, spec)
|
||||
* @param defaultSpec default spec supplier used when callers pass {@code null}
|
||||
* spec
|
||||
* @param defaultSpec supplier of the default spec used when callers pass
|
||||
* {@code null}; capability metadata resolves one stable
|
||||
* value during registration, while runtime creation retains
|
||||
* the supplier contract
|
||||
* @param <C> context type
|
||||
* @param <K> key type
|
||||
* @param <S> spec type
|
||||
* @throws NullPointerException if any class/factory/supplier argument is
|
||||
* {@code null}
|
||||
* @throws NullPointerException if any class, factory, supplier, or metadata
|
||||
* default value is {@code null}
|
||||
*/
|
||||
protected <C extends CryptoContext, K extends Key, S extends ContextSpec> void capability(AlgorithmFamily family,
|
||||
KeyUsage role, Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextConstructorKS<C, K, S> factory,
|
||||
KeyUsage role, Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextFactoryKS<C, K, S> factory,
|
||||
Supplier<? extends S> defaultSpec) {
|
||||
|
||||
S resolvedDefault = Objects.requireNonNull(defaultSpec.get(), "defaultSpec value must not be null");
|
||||
// bind runtime factory
|
||||
bind(role, ctxType, keyType, specType, factory, defaultSpec);
|
||||
bindContext(role, ctxType, keyType, specType, factory, defaultSpec);
|
||||
// publish metadata
|
||||
addCapability(new Capability(id(), family, role, ctxType, keyType, specType, defaultSpec));
|
||||
addCapability(new Capability(id(), family, role, ctxType, keyType, specType, resolvedDefault));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
package zeroecho.core.alg.aes;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
@@ -45,7 +45,9 @@ import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||
import zeroecho.core.context.EncryptionContext;
|
||||
import zeroecho.core.spec.VoidSpec;
|
||||
import zeroecho.core.spi.SymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.SymmetricKeyGenerator;
|
||||
import zeroecho.core.spi.SymmetricKeyImporter;
|
||||
import zeroecho.sdk.util.RandomSupport;
|
||||
|
||||
/**
|
||||
* AES algorithm registration and capability wiring.
|
||||
@@ -85,49 +87,45 @@ public final class AesAlgorithm extends AbstractCryptoAlgorithm {
|
||||
|
||||
// Context capabilities
|
||||
capability(AlgorithmFamily.SYMMETRIC, KeyUsage.ENCRYPT, EncryptionContext.class, SecretKey.class, AesSpec.class,
|
||||
(SecretKey k, AesSpec s) -> new AesCipherContext(this, k, true, s, new SecureRandom()),
|
||||
(SecretKey k, AesSpec s) -> new AesCipherContext(this, k, true, s, RandomSupport.getRandom()),
|
||||
() -> AesSpec.gcm128(null));
|
||||
|
||||
capability(AlgorithmFamily.SYMMETRIC, KeyUsage.DECRYPT, EncryptionContext.class, SecretKey.class, AesSpec.class,
|
||||
(SecretKey k, AesSpec s) -> new AesCipherContext(this, k, false, s, new SecureRandom()),
|
||||
(SecretKey k, AesSpec s) -> new AesCipherContext(this, k, false, s, RandomSupport.getRandom()),
|
||||
() -> AesSpec.gcm128(null));
|
||||
|
||||
capability(AlgorithmFamily.SYMMETRIC, KeyUsage.ENCRYPT, EncryptionContext.class, SecretKey.class,
|
||||
VoidSpec.class, (SecretKey k, VoidSpec s) -> new AesCipherContext(this, k, true, AesSpec.gcm128(null),
|
||||
new SecureRandom()),
|
||||
RandomSupport.getRandom()),
|
||||
() -> VoidSpec.INSTANCE);
|
||||
|
||||
capability(AlgorithmFamily.SYMMETRIC, KeyUsage.DECRYPT, EncryptionContext.class, SecretKey.class,
|
||||
VoidSpec.class, (SecretKey k, VoidSpec s) -> new AesCipherContext(this, k, false, AesSpec.gcm128(null),
|
||||
new SecureRandom()),
|
||||
RandomSupport.getRandom()),
|
||||
() -> VoidSpec.INSTANCE);
|
||||
|
||||
// Secret generation builder (AesKeyGenSpec)
|
||||
registerSymmetricKeyBuilder(AesKeyGenSpec.class, new SymmetricKeyBuilder<>() {
|
||||
registerSymmetricKeyGenerator(AesKeyGenSpec.class, new SymmetricKeyGenerator<>() {
|
||||
@Override
|
||||
public SecretKey generateSecret(AesKeyGenSpec spec) throws GeneralSecurityException {
|
||||
KeyGenerator kg = KeyGenerator.getInstance("AES");
|
||||
kg.init(spec.keySizeBits(), new SecureRandom());
|
||||
kg.init(spec.keySizeBits(), RandomSupport.getRandom());
|
||||
return kg.generateKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecretKey importSecret(AesKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException("Use AesKeyImportSpec for importing AES keys");
|
||||
}
|
||||
}, AesKeyGenSpec::aes256);
|
||||
|
||||
// Secret import builder (AesKeyImportSpec)
|
||||
registerSymmetricKeyBuilder(AesKeyImportSpec.class, new SymmetricKeyBuilder<>() {
|
||||
@Override
|
||||
public SecretKey generateSecret(AesKeyImportSpec spec) {
|
||||
throw new UnsupportedOperationException("Use AesKeyGenSpec to generate AES keys");
|
||||
}
|
||||
registerSymmetricKeyImporter(AesKeyImportSpec.class, new SymmetricKeyImporter<>() {
|
||||
|
||||
@Override
|
||||
public SecretKey importSecret(AesKeyImportSpec spec) {
|
||||
return new SecretKeySpec(spec.key(), "AES");
|
||||
byte[] key = spec.key();
|
||||
try {
|
||||
return new SecretKeySpec(key, "AES");
|
||||
} finally {
|
||||
Arrays.fill(key, (byte) 0);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ import zeroecho.core.err.ProviderFailureException;
|
||||
import zeroecho.core.io.CipherTransformInputStreamBuilder;
|
||||
import zeroecho.core.spi.ContextAware;
|
||||
import zeroecho.core.util.Strings;
|
||||
import zeroecho.sdk.util.RandomSupport;
|
||||
|
||||
/**
|
||||
* Streaming AES cipher context for GCM / CBC / CTR.
|
||||
@@ -97,7 +98,8 @@ public final class AesCipherContext implements EncryptionContext, ContextAware {
|
||||
* ({@code false})
|
||||
* @param spec static AES settings (mode/padding and GCM tag bits); not
|
||||
* null
|
||||
* @param rnd secure random source; if null, a default is created
|
||||
* @param rnd secure random source; if null, the library's shared source
|
||||
* is used
|
||||
* @throws NullPointerException if any required parameter is null
|
||||
* @throws IllegalArgumentException if {@code spec} is inconsistent (e.g., GCM
|
||||
* without NOPADDING)
|
||||
@@ -107,7 +109,7 @@ public final class AesCipherContext implements EncryptionContext, ContextAware {
|
||||
this.key = Objects.requireNonNull(key, "secret key must not be null");
|
||||
this.encrypt = encrypt;
|
||||
this.spec = Objects.requireNonNull(spec, "spec must not be null");
|
||||
this.rnd = (rnd != null ? rnd : new SecureRandom());
|
||||
this.rnd = (rnd != null ? rnd : RandomSupport.getRandom());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,6 +38,9 @@ import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
@@ -48,8 +51,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* <p>
|
||||
* This class wraps raw key material (16, 24, or 32 bytes) for use with the AES
|
||||
* algorithm. Factory methods support construction from raw bytes, hex strings,
|
||||
* or Base64-encoded strings. The key material is defensively copied to maintain
|
||||
* immutability.
|
||||
* or Base64-encoded strings. The key material is defensively copied.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
@@ -58,13 +60,16 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Objects of this type are immutable and thread-safe.
|
||||
* Objects of this type are thread-safe while active and may be destroyed to wipe
|
||||
* their owned key bytes. Access and marshalling fail after destruction.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class AesKeyImportSpec implements AlgorithmKeySpec {
|
||||
public final class AesKeyImportSpec implements AlgorithmKeySpec, Destroyable {
|
||||
private final byte[] key;
|
||||
private final ReentrantLock lifecycleLock = new ReentrantLock();
|
||||
private boolean destroyed;
|
||||
|
||||
private AesKeyImportSpec(byte[] key) {
|
||||
Objects.requireNonNull(key, "key must not be null");
|
||||
@@ -96,7 +101,12 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec {
|
||||
*/
|
||||
public static AesKeyImportSpec fromHex(String hex) {
|
||||
Objects.requireNonNull(hex, "hex must not be null");
|
||||
return fromRaw(HexFormat.of().parseHex(hex));
|
||||
byte[] decoded = HexFormat.of().parseHex(hex);
|
||||
try {
|
||||
return fromRaw(decoded);
|
||||
} finally {
|
||||
Arrays.fill(decoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,7 +119,12 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec {
|
||||
*/
|
||||
public static AesKeyImportSpec fromBase64(String b64) {
|
||||
Objects.requireNonNull(b64, "base64 must not be null");
|
||||
return fromRaw(Base64.getDecoder().decode(b64));
|
||||
byte[] decoded = Base64.getDecoder().decode(b64);
|
||||
try {
|
||||
return fromRaw(decoded);
|
||||
} finally {
|
||||
Arrays.fill(decoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,7 +133,13 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec {
|
||||
* @return the raw key material
|
||||
*/
|
||||
public byte[] key() {
|
||||
return Arrays.copyOf(key, key.length);
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return Arrays.copyOf(key, key.length);
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +150,7 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec {
|
||||
* @return a sequence containing the key data
|
||||
*/
|
||||
public static PairSeq marshal(AesKeyImportSpec spec) {
|
||||
String k = Base64.getEncoder().withoutPadding().encodeToString(spec.key);
|
||||
String k = spec.encodedKey();
|
||||
return PairSeq.of("type", "AES-KEY", "k.b64", k);
|
||||
}
|
||||
|
||||
@@ -143,21 +164,81 @@ public final class AesKeyImportSpec implements AlgorithmKeySpec {
|
||||
*/
|
||||
public static AesKeyImportSpec unmarshal(PairSeq p) {
|
||||
byte[] out = null;
|
||||
PairSeq.Cursor cur = p.cursor();
|
||||
while (cur.next()) {
|
||||
String k = cur.key();
|
||||
String v = cur.value();
|
||||
switch (k) {
|
||||
case "k.b64" -> out = Base64.getDecoder().decode(v);
|
||||
case "k.hex" -> out = HexFormat.of().parseHex(v);
|
||||
case "k.raw" -> out = v.getBytes(StandardCharsets.ISO_8859_1);
|
||||
default -> {
|
||||
/* ignore */ }
|
||||
try {
|
||||
PairSeq.Cursor cur = p.cursor();
|
||||
while (cur.next()) {
|
||||
String k = cur.key();
|
||||
String v = cur.value();
|
||||
switch (k) {
|
||||
case "k.b64" -> {
|
||||
wipe(out);
|
||||
out = Base64.getDecoder().decode(v);
|
||||
}
|
||||
case "k.hex" -> {
|
||||
wipe(out);
|
||||
out = HexFormat.of().parseHex(v);
|
||||
}
|
||||
case "k.raw" -> {
|
||||
wipe(out);
|
||||
out = v.getBytes(StandardCharsets.ISO_8859_1);
|
||||
}
|
||||
default -> {
|
||||
/* ignore */ }
|
||||
}
|
||||
}
|
||||
if (out == null) {
|
||||
throw new IllegalArgumentException("AES key missing (k.b64 / k.hex / k.raw)");
|
||||
}
|
||||
return new AesKeyImportSpec(out);
|
||||
} finally {
|
||||
wipe(out);
|
||||
}
|
||||
if (out == null) {
|
||||
throw new IllegalArgumentException("AES key missing (k.b64 / k.hex / k.raw)");
|
||||
}
|
||||
|
||||
private static void wipe(byte[] current) {
|
||||
if (current != null) {
|
||||
Arrays.fill(current, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
private String encodedKey() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return Base64.getEncoder().withoutPadding().encodeToString(key);
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public void destroy() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
if (!destroyed) {
|
||||
Arrays.fill(key, (byte) 0);
|
||||
destroyed = true;
|
||||
}
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
return destroyed;
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureActive() {
|
||||
if (destroyed) {
|
||||
throw new IllegalStateException("AES key import specification has been destroyed");
|
||||
}
|
||||
return new AesKeyImportSpec(out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
|
||||
@@ -56,7 +57,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter;
|
||||
import zeroecho.core.context.KemContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.core.spec.VoidSpec;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.AsymmetricKeyPairGenerator;
|
||||
import zeroecho.core.spi.PrivateKeyImporter;
|
||||
import zeroecho.core.spi.PublicKeyImporter;
|
||||
|
||||
/**
|
||||
* <h2>Integration of BIKE (Bit Flipping Key Encapsulation) algorithm</h2>
|
||||
@@ -84,14 +87,14 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
* BikeAlgorithm bike = new BikeAlgorithm();
|
||||
*
|
||||
* // Generate a key pair
|
||||
* KeyPair kp = bike.asymmetricKeyBuilder(BikeKeyGenSpec.class)
|
||||
* KeyPair kp = bike.asymmetricKeyPairGenerator(BikeKeyGenSpec.class)
|
||||
* .generateKeyPair(BikeKeyGenSpec.bike256());
|
||||
*
|
||||
* // Encapsulation using recipient's public key
|
||||
* KemContext kemEnc = bike.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
|
||||
* KemContext kemEnc = bike.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
|
||||
*
|
||||
* // Decapsulation using private key
|
||||
* KemContext kemDec = bike.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
|
||||
* KemContext kemDec = bike.createContext(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
|
||||
* }</pre>
|
||||
*
|
||||
* @since 1.0
|
||||
@@ -139,7 +142,7 @@ public final class BikeAlgorithm extends AbstractCryptoAlgorithm {
|
||||
.build();
|
||||
}, () -> VoidSpec.INSTANCE);
|
||||
|
||||
registerAsymmetricKeyBuilder(BikeKeyGenSpec.class, new AsymmetricKeyBuilder<>() {
|
||||
registerAsymmetricKeyPairGenerator(BikeKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(BikeKeyGenSpec spec) throws GeneralSecurityException {
|
||||
ensureProvider();
|
||||
@@ -152,23 +155,9 @@ public final class BikeAlgorithm extends AbstractCryptoAlgorithm {
|
||||
kpg.initialize(params, new SecureRandom());
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(BikeKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(BikeKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}, BikeKeyGenSpec::bike256);
|
||||
|
||||
registerAsymmetricKeyBuilder(BikePublicKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(BikePublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
registerPublicKeyImporter(BikePublicKeySpec.class, new PublicKeyImporter<>() {
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(BikePublicKeySpec spec) throws GeneralSecurityException {
|
||||
@@ -176,31 +165,22 @@ public final class BikeAlgorithm extends AbstractCryptoAlgorithm {
|
||||
KeyFactory kf = KeyFactory.getInstance("BIKE", providerName());
|
||||
return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(BikePublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}, null);
|
||||
|
||||
registerAsymmetricKeyBuilder(BikePrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(BikePrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(BikePrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
registerPrivateKeyImporter(BikePrivateKeySpec.class, new PrivateKeyImporter<>() {
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(BikePrivateKeySpec spec) throws GeneralSecurityException {
|
||||
ensureProvider();
|
||||
KeyFactory kf = KeyFactory.getInstance("BIKE", providerName());
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8()));
|
||||
byte[] encoded = spec.pkcs8();
|
||||
try {
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
|
||||
} finally {
|
||||
Arrays.fill(encoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,7 +44,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* <h3>Usage</h3> <pre>{@code
|
||||
* // Generate a BIKE-192 key pair
|
||||
* KeyPair kp = bikeAlgorithm.asymmetricKeyBuilder(BikeKeyGenSpec.class)
|
||||
* KeyPair kp = bikeAlgorithm.asymmetricKeyPairGenerator(BikeKeyGenSpec.class)
|
||||
* .generateKeyPair(BikeKeyGenSpec.bike192());
|
||||
* }</pre>
|
||||
*
|
||||
|
||||
@@ -33,8 +33,12 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.bike;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.marshal.PairSeq.Cursor;
|
||||
@@ -49,7 +53,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* <h3>Usage</h3> <pre>{@code
|
||||
* // Import a BIKE private key
|
||||
* BikePrivateKeySpec spec = new BikePrivateKeySpec(pkcs8Bytes);
|
||||
* PrivateKey key = bikeAlgorithm.importPrivate(spec);
|
||||
* PrivateKey key = bikeAlgorithm.privateKeyImporter(BikePrivateKeySpec.class).importPrivate(spec);
|
||||
*
|
||||
* // Marshal for storage or transport
|
||||
* PairSeq seq = BikePrivateKeySpec.marshal(spec);
|
||||
@@ -60,10 +64,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class BikePrivateKeySpec implements AlgorithmKeySpec {
|
||||
public final class BikePrivateKeySpec implements AlgorithmKeySpec, Destroyable {
|
||||
|
||||
private static final String PKCS8_B64 = "pkcs8.b64";
|
||||
private final byte[] pkcs8;
|
||||
private final ReentrantLock lifecycleLock = new ReentrantLock();
|
||||
private boolean destroyed;
|
||||
|
||||
/**
|
||||
* Constructs a new spec from a PKCS#8 encoded private key.
|
||||
@@ -81,7 +87,13 @@ public final class BikePrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return cloned PKCS#8 bytes
|
||||
*/
|
||||
public byte[] pkcs8() {
|
||||
return pkcs8.clone();
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return pkcs8.clone();
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,7 +111,7 @@ public final class BikePrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return serialized key representation
|
||||
*/
|
||||
public static PairSeq marshal(BikePrivateKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
|
||||
String b64 = spec.encodedKey();
|
||||
return PairSeq.of("type", "BikePrivateKeySpec", PKCS8_B64, b64);
|
||||
}
|
||||
|
||||
@@ -120,7 +132,12 @@ public final class BikePrivateKeySpec implements AlgorithmKeySpec {
|
||||
if (b64 == null) {
|
||||
throw new IllegalArgumentException("BikePrivateKeySpec: missing pkcs8.b64");
|
||||
}
|
||||
return new BikePrivateKeySpec(Base64.getDecoder().decode(b64));
|
||||
byte[] decoded = Base64.getDecoder().decode(b64);
|
||||
try {
|
||||
return new BikePrivateKeySpec(decoded);
|
||||
} finally {
|
||||
Arrays.fill(decoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,4 +149,43 @@ public final class BikePrivateKeySpec implements AlgorithmKeySpec {
|
||||
public String toString() {
|
||||
return "BikePrivateKeySpec[len=" + pkcs8.length + "]";
|
||||
}
|
||||
|
||||
private String encodedKey() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
if (!destroyed) {
|
||||
Arrays.fill(pkcs8, (byte) 0);
|
||||
destroyed = true;
|
||||
}
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
return destroyed;
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureActive() {
|
||||
if (destroyed) {
|
||||
throw new IllegalStateException("BIKE private key specification has been destroyed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* <h3>Usage</h3> <pre>{@code
|
||||
* // Import a BIKE public key
|
||||
* BikePublicKeySpec spec = new BikePublicKeySpec(x509Bytes);
|
||||
* PublicKey key = bikeAlgorithm.importPublic(spec);
|
||||
* PublicKey key = bikeAlgorithm.publicKeyImporter(BikePublicKeySpec.class).importPublic(spec);
|
||||
*
|
||||
* // Marshal for transport or storage
|
||||
* PairSeq seq = BikePublicKeySpec.marshal(spec);
|
||||
|
||||
@@ -35,13 +35,15 @@ package zeroecho.core.alg.chacha;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||
import zeroecho.core.spi.SymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.SymmetricKeyGenerator;
|
||||
import zeroecho.core.spi.SymmetricKeyImporter;
|
||||
|
||||
/**
|
||||
* <h2>Abstract base for ChaCha family algorithms</h2>
|
||||
@@ -64,19 +66,20 @@ import zeroecho.core.spi.SymmetricKeyBuilder;
|
||||
* {@code "ChaCha20"}.</li>
|
||||
* <li>Import wraps the raw key material with
|
||||
* {@link javax.crypto.spec.SecretKeySpec}.</li>
|
||||
* <li>Attempts to generate a key via {@code ChaChaKeyImportSpec} or import via
|
||||
* {@code ChaChaKeyGenSpec} will throw
|
||||
* {@link UnsupportedOperationException}.</li>
|
||||
* <li>Generation and import are discovered through independent exact
|
||||
* capabilities, so an unsupported lookup fails before invocation.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>Example</h3> <pre>{@code
|
||||
* AbstractChaChaAlgorithm algo = ...;
|
||||
* ZeroEchoSession session = new ZeroEchoSession();
|
||||
*
|
||||
* // Generate a fresh 256-bit key
|
||||
* SecretKey key = algo.generateSecret(ChaChaKeyGenSpec.chacha256());
|
||||
* SecretKey key = session.keyBuilders().symmetric()
|
||||
* .generate("ChaCha20", ChaChaKeyGenSpec.chacha256());
|
||||
*
|
||||
* // Import an existing key
|
||||
* SecretKey imported = algo.importSecret(new ChaChaKeyImportSpec(rawBytes));
|
||||
* SecretKey imported = session.keyBuilders().symmetric()
|
||||
* .importKey("ChaCha20", new ChaChaKeyImportSpec(rawBytes));
|
||||
* }</pre>
|
||||
*
|
||||
* @since 1.0
|
||||
@@ -93,30 +96,26 @@ abstract class AbstractChaChaAlgorithm extends AbstractCryptoAlgorithm {
|
||||
super(id, title);
|
||||
|
||||
// register once for both algorithms (same 256-bit key)
|
||||
registerSymmetricKeyBuilder(ChaChaKeyGenSpec.class, new SymmetricKeyBuilder<>() {
|
||||
registerSymmetricKeyGenerator(ChaChaKeyGenSpec.class, new SymmetricKeyGenerator<>() {
|
||||
@Override
|
||||
public SecretKey generateSecret(ChaChaKeyGenSpec spec) throws GeneralSecurityException {
|
||||
KeyGenerator kg = KeyGenerator.getInstance("ChaCha20");
|
||||
kg.init(spec.keySizeBits(), new SecureRandom());
|
||||
return kg.generateKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecretKey importSecret(ChaChaKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException("Use ChaChaKeyImportSpec for importing ChaCha keys");
|
||||
}
|
||||
}, ChaChaKeyGenSpec::chacha256);
|
||||
|
||||
registerSymmetricKeyBuilder(ChaChaKeyImportSpec.class, new SymmetricKeyBuilder<>() {
|
||||
@Override
|
||||
public SecretKey generateSecret(ChaChaKeyImportSpec spec) {
|
||||
throw new UnsupportedOperationException("Use ChaChaKeyGenSpec to generate ChaCha keys");
|
||||
}
|
||||
registerSymmetricKeyImporter(ChaChaKeyImportSpec.class, new SymmetricKeyImporter<>() {
|
||||
|
||||
@Override
|
||||
public SecretKey importSecret(ChaChaKeyImportSpec spec) {
|
||||
return new SecretKeySpec(spec.key(), "ChaCha20");
|
||||
byte[] key = spec.key();
|
||||
try {
|
||||
return new SecretKeySpec(key, "ChaCha20");
|
||||
} finally {
|
||||
Arrays.fill(key, (byte) 0);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,19 +74,19 @@ import zeroecho.core.SymmetricHeaderCodec;
|
||||
* corresponding cipher context.
|
||||
*
|
||||
* <h3>Example</h3> <pre>{@code
|
||||
* var algo = new ChaCha20Poly1305Algorithm();
|
||||
* SecretKey key = algo.generateSecret(ChaChaKeyGenSpec.chacha256());
|
||||
* ZeroEchoSession session = new ZeroEchoSession();
|
||||
* SecretKey key = session.keyBuilders().symmetric()
|
||||
* .generate("CHACHA20-POLY1305", ChaChaKeyGenSpec.chacha256());
|
||||
*
|
||||
* // Encrypt with explicit spec
|
||||
* var spec = ChaCha20Poly1305Spec.builder().header(null).build();
|
||||
* EncryptionContext enc = algo.newContext(
|
||||
* zeroecho.core.AlgorithmFamily.SYMMETRIC,
|
||||
* zeroecho.core.KeyUsage.ENCRYPT, key, spec);
|
||||
* ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(null).build();
|
||||
* EncryptionContext enc = session.createContext(
|
||||
* "CHACHA20-POLY1305", zeroecho.core.KeyUsage.ENCRYPT, key, spec);
|
||||
*
|
||||
* // Decrypt using VoidSpec default
|
||||
* EncryptionContext dec = algo.newContext(
|
||||
* zeroecho.core.AlgorithmFamily.SYMMETRIC,
|
||||
* zeroecho.core.KeyUsage.DECRYPT, key, zeroecho.core.spec.VoidSpec.INSTANCE);
|
||||
* EncryptionContext dec = session.createContext(
|
||||
* "CHACHA20-POLY1305", zeroecho.core.KeyUsage.DECRYPT, key,
|
||||
* zeroecho.core.spec.VoidSpec.INSTANCE);
|
||||
* }</pre>
|
||||
*
|
||||
* @since 1.0
|
||||
|
||||
@@ -38,6 +38,9 @@ import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
@@ -66,7 +69,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* <h3>Usage</h3> <pre>{@code
|
||||
* // Import from raw key bytes
|
||||
* ChaChaKeyImportSpec spec = ChaChaKeyImportSpec.fromRaw(keyBytes);
|
||||
* SecretKey key = cryptoAlgorithm.importSecret(spec);
|
||||
* SecretKey key = cryptoAlgorithm.symmetricKeyImporter(ChaChaKeyImportSpec.class).importSecret(spec);
|
||||
*
|
||||
* // Serialize to PairSeq
|
||||
* PairSeq seq = ChaChaKeyImportSpec.marshal(spec);
|
||||
@@ -77,8 +80,10 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
|
||||
public final class ChaChaKeyImportSpec implements AlgorithmKeySpec, Destroyable {
|
||||
private final byte[] key;
|
||||
private final ReentrantLock lifecycleLock = new ReentrantLock();
|
||||
private boolean destroyed;
|
||||
|
||||
/**
|
||||
* Creates a new import spec with the given raw key.
|
||||
@@ -112,7 +117,12 @@ public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
|
||||
* @return spec wrapping the decoded key
|
||||
*/
|
||||
public static ChaChaKeyImportSpec fromHex(String hex) {
|
||||
return fromRaw(HexFormat.of().parseHex(hex));
|
||||
byte[] decoded = HexFormat.of().parseHex(hex);
|
||||
try {
|
||||
return fromRaw(decoded);
|
||||
} finally {
|
||||
Arrays.fill(decoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,7 +132,12 @@ public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
|
||||
* @return spec wrapping the decoded key
|
||||
*/
|
||||
public static ChaChaKeyImportSpec fromBase64(String b64) {
|
||||
return fromRaw(Base64.getDecoder().decode(b64));
|
||||
byte[] decoded = Base64.getDecoder().decode(b64);
|
||||
try {
|
||||
return fromRaw(decoded);
|
||||
} finally {
|
||||
Arrays.fill(decoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,7 +146,13 @@ public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
|
||||
* @return 32-byte key array
|
||||
*/
|
||||
public byte[] key() {
|
||||
return Arrays.copyOf(key, key.length);
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return Arrays.copyOf(key, key.length);
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +162,7 @@ public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
|
||||
* @return serialized key representation
|
||||
*/
|
||||
public static PairSeq marshal(ChaChaKeyImportSpec spec) {
|
||||
String k = Base64.getEncoder().withoutPadding().encodeToString(spec.key);
|
||||
String k = spec.encodedKey();
|
||||
return PairSeq.of("type", "CHACHA-KEY", "k.b64", k);
|
||||
}
|
||||
|
||||
@@ -163,21 +184,81 @@ public final class ChaChaKeyImportSpec implements AlgorithmKeySpec {
|
||||
*/
|
||||
public static ChaChaKeyImportSpec unmarshal(PairSeq p) {
|
||||
byte[] out = null;
|
||||
PairSeq.Cursor c = p.cursor();
|
||||
while (c.next()) {
|
||||
String k = c.key();
|
||||
String v = c.value();
|
||||
switch (k) {
|
||||
case "k.b64" -> out = Base64.getDecoder().decode(v);
|
||||
case "k.hex" -> out = HexFormat.of().parseHex(v);
|
||||
case "k.raw" -> out = v.getBytes(StandardCharsets.ISO_8859_1);
|
||||
default -> {
|
||||
try {
|
||||
PairSeq.Cursor c = p.cursor();
|
||||
while (c.next()) {
|
||||
String k = c.key();
|
||||
String v = c.value();
|
||||
switch (k) {
|
||||
case "k.b64" -> {
|
||||
wipe(out);
|
||||
out = Base64.getDecoder().decode(v);
|
||||
}
|
||||
case "k.hex" -> {
|
||||
wipe(out);
|
||||
out = HexFormat.of().parseHex(v);
|
||||
}
|
||||
case "k.raw" -> {
|
||||
wipe(out);
|
||||
out = v.getBytes(StandardCharsets.ISO_8859_1);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out == null) {
|
||||
throw new IllegalArgumentException("ChaCha20 key missing (k.b64 / k.hex / k.raw)");
|
||||
}
|
||||
return new ChaChaKeyImportSpec(out);
|
||||
} finally {
|
||||
wipe(out);
|
||||
}
|
||||
if (out == null) {
|
||||
throw new IllegalArgumentException("ChaCha20 key missing (k.b64 / k.hex / k.raw)");
|
||||
}
|
||||
|
||||
private static void wipe(byte[] current) {
|
||||
if (current != null) {
|
||||
Arrays.fill(current, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
private String encodedKey() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return Base64.getEncoder().withoutPadding().encodeToString(key);
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public void destroy() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
if (!destroyed) {
|
||||
Arrays.fill(key, (byte) 0);
|
||||
destroyed = true;
|
||||
}
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
return destroyed;
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureActive() {
|
||||
if (destroyed) {
|
||||
throw new IllegalStateException("ChaCha key import specification has been destroyed");
|
||||
}
|
||||
return new ChaChaKeyImportSpec(out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,10 @@
|
||||
* <p>
|
||||
* This package provides the ChaCha capability set for the core layer, including
|
||||
* the stream cipher ChaCha20 and the AEAD construction ChaCha20-Poly1305. The
|
||||
* module contains algorithm descriptors, streaming cipher contexts, immutable
|
||||
* specifications, optional header codecs for runtime parameters, and symmetric
|
||||
* key import/generation specifications. The design favors safe defaults
|
||||
* module contains algorithm descriptors, streaming cipher contexts,
|
||||
* configuration specifications, optional header codecs for runtime parameters,
|
||||
* and symmetric key import/generation specifications. Key import
|
||||
* specifications are destroyable. The design favors safe defaults
|
||||
* (12-byte nonces, 128-bit AEAD tag), explicit role-to-context binding, and a
|
||||
* clear separation between static configuration and per-operation parameters.
|
||||
* </p>
|
||||
|
||||
@@ -44,6 +44,7 @@ import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
|
||||
@@ -56,7 +57,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter;
|
||||
import zeroecho.core.context.KemContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.core.spec.VoidSpec;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.AsymmetricKeyPairGenerator;
|
||||
import zeroecho.core.spi.PrivateKeyImporter;
|
||||
import zeroecho.core.spi.PublicKeyImporter;
|
||||
|
||||
/**
|
||||
* <h2>Classic McEliece (CMCE) algorithm adapter</h2>
|
||||
@@ -106,15 +109,15 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
* CmceAlgorithm alg = new CmceAlgorithm();
|
||||
*
|
||||
* // Generate a key pair with a chosen CMCE variant.
|
||||
* KeyPair kp = alg.asymmetricKeyBuilder(CmceKeyGenSpec.class)
|
||||
* KeyPair kp = alg.asymmetricKeyPairGenerator(CmceKeyGenSpec.class)
|
||||
* .generateKeyPair(CmceKeyGenSpec.mceliece8192128f());
|
||||
*
|
||||
* // Create a KEM encapsulation context with the recipient public key.
|
||||
* KemContext enc = alg.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
|
||||
* KemContext enc = alg.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
|
||||
*
|
||||
* // Create an agreement initiator context backed by CMCE KEM.
|
||||
* MessageAgreementContext initiator =
|
||||
* alg.create(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE);
|
||||
* alg.createContext(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE);
|
||||
* }</pre>
|
||||
*
|
||||
* @since 1.0
|
||||
@@ -169,7 +172,7 @@ public final class CmceAlgorithm extends AbstractCryptoAlgorithm {
|
||||
.build();
|
||||
}, () -> VoidSpec.INSTANCE);
|
||||
|
||||
registerAsymmetricKeyBuilder(CmceKeyGenSpec.class, new AsymmetricKeyBuilder<>() {
|
||||
registerAsymmetricKeyPairGenerator(CmceKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(CmceKeyGenSpec spec) throws GeneralSecurityException {
|
||||
ensureProvider();
|
||||
@@ -189,23 +192,9 @@ public final class CmceAlgorithm extends AbstractCryptoAlgorithm {
|
||||
kpg.initialize(params, new SecureRandom());
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(CmceKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(CmceKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}, CmceKeyGenSpec::mceliece8192128f);
|
||||
|
||||
registerAsymmetricKeyBuilder(CmcePublicKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(CmcePublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
registerPublicKeyImporter(CmcePublicKeySpec.class, new PublicKeyImporter<>() {
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(CmcePublicKeySpec spec) throws GeneralSecurityException {
|
||||
@@ -213,31 +202,22 @@ public final class CmceAlgorithm extends AbstractCryptoAlgorithm {
|
||||
KeyFactory kf = KeyFactory.getInstance("CMCE", providerName());
|
||||
return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(CmcePublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}, null);
|
||||
|
||||
registerAsymmetricKeyBuilder(CmcePrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(CmcePrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(CmcePrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
registerPrivateKeyImporter(CmcePrivateKeySpec.class, new PrivateKeyImporter<>() {
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(CmcePrivateKeySpec spec) throws GeneralSecurityException {
|
||||
ensureProvider();
|
||||
KeyFactory kf = KeyFactory.getInstance("CMCE", providerName());
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8()));
|
||||
byte[] encoded = spec.pkcs8();
|
||||
try {
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
|
||||
} finally {
|
||||
Arrays.fill(encoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
});
|
||||
}
|
||||
|
||||
private static void ensureProvider() throws NoSuchProviderException {
|
||||
|
||||
@@ -52,7 +52,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* <pre>{@code
|
||||
* // Generate a key pair for McEliece 8192128F (256-bit security, fast)
|
||||
* CmceKeyGenSpec spec = CmceKeyGenSpec.mceliece8192128f();
|
||||
* KeyPair kp = alg.asymmetricKeyBuilder(CmceKeyGenSpec.class).generateKeyPair(spec);
|
||||
* KeyPair kp = alg.asymmetricKeyPairGenerator(CmceKeyGenSpec.class).generateKeyPair(spec);
|
||||
* }</pre>
|
||||
*
|
||||
* @since 1.0
|
||||
|
||||
@@ -33,8 +33,12 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.cmce;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.marshal.PairSeq.Cursor;
|
||||
@@ -49,8 +53,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Instances are immutable. The internal byte array is cloned on construction
|
||||
* and on every accessor to prevent accidental mutation.
|
||||
* The internal byte array is cloned on construction and on every accessor.
|
||||
* Access and destruction are synchronized.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Marshalling</h2>
|
||||
@@ -76,10 +80,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class CmcePrivateKeySpec implements AlgorithmKeySpec {
|
||||
public final class CmcePrivateKeySpec implements AlgorithmKeySpec, Destroyable {
|
||||
|
||||
private static final String PKCS8_B64 = "pkcs8.b64";
|
||||
private final byte[] pkcs8;
|
||||
private final ReentrantLock lifecycleLock = new ReentrantLock();
|
||||
private boolean destroyed;
|
||||
|
||||
/**
|
||||
* Creates a new specification from a PKCS#8-encoded CMCE private key.
|
||||
@@ -101,7 +107,13 @@ public final class CmcePrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return a fresh copy of the underlying PKCS#8 encoding
|
||||
*/
|
||||
public byte[] pkcs8() {
|
||||
return pkcs8.clone();
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return pkcs8.clone();
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,7 +130,7 @@ public final class CmcePrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @throws NullPointerException if {@code spec} is null
|
||||
*/
|
||||
public static PairSeq marshal(CmcePrivateKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
|
||||
String b64 = spec.encodedKey();
|
||||
return PairSeq.of("type", "CmcePrivateKeySpec", PKCS8_B64, b64);
|
||||
}
|
||||
|
||||
@@ -144,7 +156,12 @@ public final class CmcePrivateKeySpec implements AlgorithmKeySpec {
|
||||
if (b64 == null) {
|
||||
throw new IllegalArgumentException("CmcePrivateKeySpec: missing pkcs8.b64");
|
||||
}
|
||||
return new CmcePrivateKeySpec(Base64.getDecoder().decode(b64));
|
||||
byte[] decoded = Base64.getDecoder().decode(b64);
|
||||
try {
|
||||
return new CmcePrivateKeySpec(decoded);
|
||||
} finally {
|
||||
Arrays.fill(decoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,4 +177,43 @@ public final class CmcePrivateKeySpec implements AlgorithmKeySpec {
|
||||
public String toString() {
|
||||
return "CmcePrivateKeySpec[len=" + pkcs8.length + "]";
|
||||
}
|
||||
|
||||
private String encodedKey() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
if (!destroyed) {
|
||||
Arrays.fill(pkcs8, (byte) 0);
|
||||
destroyed = true;
|
||||
}
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
return destroyed;
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureActive() {
|
||||
if (destroyed) {
|
||||
throw new IllegalStateException("CMCE private key specification has been destroyed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +67,8 @@
|
||||
* selects a CMCE parameter set (variant) used by the key-pair builder.</li>
|
||||
* <li><b>Key import specs:</b> {@link zeroecho.core.alg.cmce.CmcePublicKeySpec}
|
||||
* wraps X.509 public keys and {@link zeroecho.core.alg.cmce.CmcePrivateKeySpec}
|
||||
* wraps PKCS#8 private keys; both are immutable and defensively copy their byte
|
||||
* arrays.</li>
|
||||
* wraps PKCS#8 private keys; both defensively copy their byte arrays, and the
|
||||
* private-key form is destroyable.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Provider requirements</h2>
|
||||
|
||||
@@ -33,21 +33,18 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.common.agreement;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.Key;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
|
||||
import javax.crypto.KeyAgreement;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
|
||||
/**
|
||||
* <h2>Generic JCA-based Key Agreement Context</h2>
|
||||
*
|
||||
* An {@link AgreementContext} backed by the standard JCA {@link KeyAgreement}
|
||||
* API. This class supports elliptic-curve and modern Diffie-Hellman variants
|
||||
* An {@link AgreementContext} backed by the standard JCA key-agreement API.
|
||||
* This class supports elliptic-curve and modern Diffie-Hellman variants
|
||||
* such as ECDH, XDH (X25519, X448), and others provided by the runtime or
|
||||
* configured provider.
|
||||
*
|
||||
@@ -75,12 +72,9 @@ import zeroecho.core.context.AgreementContext;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public class GenericJcaAgreementContext implements AgreementContext {
|
||||
public final class GenericJcaAgreementContext implements AgreementContext {
|
||||
private final CryptoAlgorithm algorithm;
|
||||
private final PrivateKey privateKey;
|
||||
private final String jcaName; // e.g., "ECDH" or "XDH" (or "X25519"/"X448")
|
||||
private final String provider; // null => default
|
||||
private PublicKey peer;
|
||||
private final JcaAgreementEngine engine;
|
||||
|
||||
/**
|
||||
* Creates a new JCA-based agreement context.
|
||||
@@ -95,10 +89,8 @@ public class GenericJcaAgreementContext implements AgreementContext {
|
||||
* is {@code null}
|
||||
*/
|
||||
public GenericJcaAgreementContext(CryptoAlgorithm alg, PrivateKey priv, String jcaName, String provider) {
|
||||
this.algorithm = alg;
|
||||
this.privateKey = priv;
|
||||
this.jcaName = jcaName;
|
||||
this.provider = provider;
|
||||
this.algorithm = java.util.Objects.requireNonNull(alg, "alg must not be null");
|
||||
this.engine = new JcaAgreementEngine(priv, jcaName, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,7 +110,7 @@ public class GenericJcaAgreementContext implements AgreementContext {
|
||||
*/
|
||||
@Override
|
||||
public Key key() {
|
||||
return privateKey;
|
||||
return engine.privateKey();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,7 +125,7 @@ public class GenericJcaAgreementContext implements AgreementContext {
|
||||
*/
|
||||
@Override
|
||||
public void setPeerPublic(PublicKey peer) {
|
||||
this.peer = peer;
|
||||
engine.setPeerPublic(peer);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +133,7 @@ public class GenericJcaAgreementContext implements AgreementContext {
|
||||
* previously assigned peer public key.
|
||||
*
|
||||
* <p>
|
||||
* Internally this delegates to the JCA {@link KeyAgreement} API with the given
|
||||
* Internally this delegates to the JCA key-agreement API with the given
|
||||
* {@code jcaName} and optional provider.
|
||||
* </p>
|
||||
*
|
||||
@@ -152,18 +144,7 @@ public class GenericJcaAgreementContext implements AgreementContext {
|
||||
*/
|
||||
@Override
|
||||
public byte[] deriveSecret() {
|
||||
if (peer == null) {
|
||||
throw new IllegalStateException("Peer public key not set");
|
||||
}
|
||||
try {
|
||||
KeyAgreement ka = (provider == null) ? KeyAgreement.getInstance(jcaName)
|
||||
: KeyAgreement.getInstance(jcaName, provider);
|
||||
ka.init(privateKey);
|
||||
ka.doPhase(peer, true);
|
||||
return ka.generateSecret();
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalArgumentException("KeyAgreement failed for " + jcaName, e);
|
||||
}
|
||||
return engine.deriveSecret();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -104,9 +104,9 @@ import zeroecho.core.context.MessageAgreementContext;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class GenericJcaMessageAgreementContext extends GenericJcaAgreementContext
|
||||
implements MessageAgreementContext {
|
||||
public final class GenericJcaMessageAgreementContext implements MessageAgreementContext {
|
||||
|
||||
private final GenericJcaAgreementContext agreement;
|
||||
private final PublicKey localPublic;
|
||||
private final String keyFactoryAlg;
|
||||
private final String keyFactoryProvider;
|
||||
@@ -139,7 +139,8 @@ public final class GenericJcaMessageAgreementContext extends GenericJcaAgreement
|
||||
*/
|
||||
public GenericJcaMessageAgreementContext(CryptoAlgorithm alg, KeyPairKey keyPairKey, String jcaAgreementName,
|
||||
String agreementProvider, String keyFactoryAlg, String keyFactoryProvider) {
|
||||
super(Objects.requireNonNull(alg, "alg"), Objects.requireNonNull(keyPairKey, "keyPairKey").privateKey(),
|
||||
this.agreement = new GenericJcaAgreementContext(Objects.requireNonNull(alg, "alg"),
|
||||
Objects.requireNonNull(keyPairKey, "keyPairKey").privateKey(),
|
||||
Objects.requireNonNull(jcaAgreementName, "jcaAgreementName"), agreementProvider);
|
||||
this.localPublic = Objects.requireNonNull(keyPairKey.publicKey(), "keyPairKey.public");
|
||||
this.keyFactoryAlg = Objects.requireNonNull(keyFactoryAlg, "keyFactoryAlg");
|
||||
@@ -199,12 +200,18 @@ public final class GenericJcaMessageAgreementContext extends GenericJcaAgreement
|
||||
@Override
|
||||
public void setPeerMessage(byte[] message) {
|
||||
if (message == null) {
|
||||
setPeerPublic(null);
|
||||
agreement.setPeerPublic(null);
|
||||
return;
|
||||
}
|
||||
|
||||
PublicKey peerPublic = importPeerPublic(message);
|
||||
setPeerPublic(peerPublic);
|
||||
agreement.setPeerPublic(peerPublic);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public void setPeerPublic(PublicKey peer) {
|
||||
agreement.setPeerPublic(peer);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,4 +239,28 @@ public final class GenericJcaMessageAgreementContext extends GenericJcaAgreement
|
||||
throw new IllegalArgumentException("Failed to import peer public key using KeyFactory " + keyFactoryAlg, e);
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public byte[] deriveSecret() {
|
||||
return agreement.deriveSecret();
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public CryptoAlgorithm algorithm() {
|
||||
return agreement.algorithm();
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public java.security.Key key() {
|
||||
return agreement.key();
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public void close() {
|
||||
agreement.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,11 +36,9 @@ package zeroecho.core.alg.common.eddsa;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.AsymmetricKeyPairGenerator;
|
||||
|
||||
/**
|
||||
* <h2>Abstract EdDSA Key-Pair Builder</h2>
|
||||
@@ -71,7 +69,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public abstract class AbstractEdDSAKeyGenBuilder<S extends AlgorithmKeySpec> implements AsymmetricKeyBuilder<S> {
|
||||
public abstract class AbstractEdDSAKeyGenBuilder<S extends AlgorithmKeySpec> implements AsymmetricKeyPairGenerator<S> {
|
||||
/**
|
||||
* Returns the JCA algorithm name understood by {@link KeyPairGenerator}.
|
||||
*
|
||||
@@ -92,9 +90,8 @@ public abstract class AbstractEdDSAKeyGenBuilder<S extends AlgorithmKeySpec> imp
|
||||
* Generates a new EdDSA key pair using JCA defaults.
|
||||
*
|
||||
* <p>
|
||||
* The provided {@code spec} is not inspected in this base implementation, but
|
||||
* it satisfies the {@link AsymmetricKeyBuilder} contract. Subclasses may extend
|
||||
* this behavior to interpret spec parameters.
|
||||
* The provided {@code spec} is not inspected in this base implementation.
|
||||
* Subclasses may extend this behavior to interpret specification parameters.
|
||||
* </p>
|
||||
*
|
||||
* @param spec algorithm-specific key specification (currently unused)
|
||||
@@ -107,38 +104,4 @@ public abstract class AbstractEdDSAKeyGenBuilder<S extends AlgorithmKeySpec> imp
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance(jcaKeyPairAlg());
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
/**
|
||||
* Always throws, as this builder does not support public key import.
|
||||
*
|
||||
* <p>
|
||||
* Importing encoded EdDSA public keys must be done through the corresponding
|
||||
* {@code *PublicKeySpec} builder class.
|
||||
* </p>
|
||||
*
|
||||
* @param spec algorithm-specific key specification
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always thrown
|
||||
*/
|
||||
@Override
|
||||
public PublicKey importPublic(S spec) {
|
||||
throw new UnsupportedOperationException("Use the corresponding PublicKeySpec to import a public key.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Always throws, as this builder does not support private key import.
|
||||
*
|
||||
* <p>
|
||||
* Importing encoded EdDSA private keys must be done through the corresponding
|
||||
* {@code *PrivateKeySpec} builder class.
|
||||
* </p>
|
||||
*
|
||||
* @param spec algorithm-specific key specification
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always thrown
|
||||
*/
|
||||
@Override
|
||||
public PrivateKey importPrivate(S spec) {
|
||||
throw new UnsupportedOperationException("Use the corresponding PrivateKeySpec to import a private key.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +35,12 @@ package zeroecho.core.alg.common.eddsa;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.PrivateKeyImporter;
|
||||
|
||||
/**
|
||||
* <h2>Abstract EdDSA Encoded Private Key Builder</h2>
|
||||
@@ -74,7 +73,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public abstract class AbstractEncodedPrivateKeyBuilder<S extends AlgorithmKeySpec> implements AsymmetricKeyBuilder<S> {
|
||||
public abstract class AbstractEncodedPrivateKeyBuilder<S extends AlgorithmKeySpec> implements PrivateKeyImporter<S> {
|
||||
/**
|
||||
* Returns the canonical JCA algorithm identifier used by
|
||||
* {@link KeyFactory#getInstance(String)}.
|
||||
@@ -101,32 +100,6 @@ public abstract class AbstractEncodedPrivateKeyBuilder<S extends AlgorithmKeySpe
|
||||
*/
|
||||
protected abstract byte[] encodedPkcs8(S spec);
|
||||
|
||||
/**
|
||||
* Unsupported in this builder, since generation is handled by the
|
||||
* {@link AbstractEdDSAKeyGenBuilder}.
|
||||
*
|
||||
* @param spec algorithm-specific key specification
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always thrown
|
||||
*/
|
||||
@Override
|
||||
public KeyPair generateKeyPair(S spec) {
|
||||
throw new UnsupportedOperationException("Generation not supported by this spec.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported in this builder, since public key import is delegated to the
|
||||
* matching {@code *PublicKeySpec} builder.
|
||||
*
|
||||
* @param spec algorithm-specific key specification
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always thrown
|
||||
*/
|
||||
@Override
|
||||
public PublicKey importPublic(S spec) {
|
||||
throw new UnsupportedOperationException("Use the corresponding PublicKeySpec.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports an EdDSA private key from its PKCS#8-encoded form.
|
||||
*
|
||||
@@ -144,6 +117,11 @@ public abstract class AbstractEncodedPrivateKeyBuilder<S extends AlgorithmKeySpe
|
||||
@Override
|
||||
public PrivateKey importPrivate(S spec) throws GeneralSecurityException {
|
||||
KeyFactory kf = KeyFactory.getInstance(jcaKeyFactoryAlg());
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(encodedPkcs8(spec)));
|
||||
byte[] encoded = encodedPkcs8(spec);
|
||||
try {
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
|
||||
} finally {
|
||||
Arrays.fill(encoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +35,11 @@ package zeroecho.core.alg.common.eddsa;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.PublicKeyImporter;
|
||||
|
||||
/**
|
||||
* <h2>Abstract EdDSA Encoded Public Key Builder</h2>
|
||||
@@ -75,7 +73,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public abstract class AbstractEncodedPublicKeyBuilder<S extends AlgorithmKeySpec> implements AsymmetricKeyBuilder<S> {
|
||||
public abstract class AbstractEncodedPublicKeyBuilder<S extends AlgorithmKeySpec> implements PublicKeyImporter<S> {
|
||||
|
||||
/**
|
||||
* Returns the canonical JCA algorithm identifier used by
|
||||
@@ -103,19 +101,6 @@ public abstract class AbstractEncodedPublicKeyBuilder<S extends AlgorithmKeySpec
|
||||
*/
|
||||
protected abstract byte[] encodedX509(S spec);
|
||||
|
||||
/**
|
||||
* Unsupported in this builder, since key pair generation is handled by
|
||||
* {@link AbstractEdDSAKeyGenBuilder}.
|
||||
*
|
||||
* @param spec algorithm-specific key specification
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always thrown
|
||||
*/
|
||||
@Override
|
||||
public KeyPair generateKeyPair(S spec) {
|
||||
throw new UnsupportedOperationException("Generation not supported by this spec.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports an EdDSA public key from its X.509-encoded form.
|
||||
*
|
||||
@@ -135,17 +120,4 @@ public abstract class AbstractEncodedPublicKeyBuilder<S extends AlgorithmKeySpec
|
||||
KeyFactory kf = KeyFactory.getInstance(jcaKeyFactoryAlg());
|
||||
return kf.generatePublic(new X509EncodedKeySpec(encodedX509(spec)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported in this builder, since private key import is delegated to the
|
||||
* matching {@code *PrivateKeySpec} builder.
|
||||
*
|
||||
* @param spec algorithm-specific key specification
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always thrown
|
||||
*/
|
||||
@Override
|
||||
public PrivateKey importPrivate(S spec) {
|
||||
throw new UnsupportedOperationException("Use the corresponding PrivateKeySpec.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ public final class GenericJcaSignatureContext implements SignatureContext {
|
||||
|
||||
// lifecycle
|
||||
private boolean wrapped; // = false;
|
||||
private Stream activeStream;
|
||||
private SignatureStream activeStream;
|
||||
private boolean autoCloseActiveStream;
|
||||
|
||||
/**
|
||||
@@ -449,7 +449,7 @@ public final class GenericJcaSignatureContext implements SignatureContext {
|
||||
|
||||
LOG.log(Level.INFO, "wrap for signing, tagLength={0}", declaredTagLen);
|
||||
|
||||
Stream s = new Stream(engine, signMode, upstream, tagLength(), expectedTag, verifier());
|
||||
SignatureStream s = new SignatureStream(engine, signMode, upstream, tagLength(), expectedTag, verifier());
|
||||
this.activeStream = s;
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ public final class SignatureInteropProfile { // NOPMD
|
||||
* @param keyAlgorithmId ZeroEcho key algorithm identifier used for key
|
||||
* import and matching, such as {@code RSA}
|
||||
* @param contextAlgorithmId ZeroEcho context algorithm identifier used
|
||||
* with {@code CryptoAlgorithms.create(...)}
|
||||
* with {@code ZeroEchoSession.createContext(...)}
|
||||
* @param contextSpec explicit ZeroEcho context specification
|
||||
* @param signatureRepresentation signature representation bridge between
|
||||
* external bytes and internal ZeroEcho bytes
|
||||
|
||||
@@ -81,8 +81,8 @@ import zeroecho.core.util.Strings;
|
||||
* upstream stream.
|
||||
* </p>
|
||||
*/
|
||||
final class Stream extends AbstractPassthroughInputStream {
|
||||
private static final Logger LOG = Logger.getLogger(Stream.class.getName());
|
||||
final class SignatureStream extends AbstractPassthroughInputStream {
|
||||
private static final Logger LOG = Logger.getLogger(SignatureStream.class.getName());
|
||||
|
||||
/** Cached trailer in sign mode: computed once from {@link Signature#sign()}. */
|
||||
private byte[] signature;
|
||||
@@ -108,7 +108,7 @@ final class Stream extends AbstractPassthroughInputStream {
|
||||
* @param strategy verification predicate used in verify mode; ignored in
|
||||
* sign mode; must not be {@code null} in verify mode
|
||||
*/
|
||||
/* package */ Stream(final Signature engine, final boolean signMode, final InputStream upstream,
|
||||
/* package */ SignatureStream(final Signature engine, final boolean signMode, final InputStream upstream,
|
||||
final int bodyBufSize, final byte[] expectedTag, final VerificationBiPredicate<Signature> strategy) {
|
||||
super(upstream, bodyBufSize);
|
||||
this.engine = engine;
|
||||
@@ -60,7 +60,7 @@
|
||||
* configured {@link java.security.Signature}, resolves a fixed tag length (via
|
||||
* resolvers), and exposes a one-shot {@code wrap(InputStream)} API.
|
||||
* Verification behavior is controlled by a pluggable comparison approach.</li>
|
||||
* <li><b>Stream</b> - internal passthrough input stream that feeds chunks to
|
||||
* <li><b>SignatureStream</b> - internal passthrough input stream that feeds chunks to
|
||||
* the signature engine, emits the trailer in SIGN mode, and performs final
|
||||
* verification in VERIFY mode.</li>
|
||||
* </ul>
|
||||
|
||||
@@ -35,11 +35,11 @@ package zeroecho.core.alg.dh;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
import zeroecho.core.AlgorithmFamily;
|
||||
import zeroecho.core.KeyUsage;
|
||||
@@ -49,7 +49,8 @@ import zeroecho.core.alg.common.agreement.GenericJcaMessageAgreementContext;
|
||||
import zeroecho.core.alg.common.agreement.KeyPairKey;
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.PrivateKeyImporter;
|
||||
import zeroecho.core.spi.PublicKeyImporter;
|
||||
|
||||
/**
|
||||
* Diffie-Hellman algorithm registration for use in the pluggable cryptography
|
||||
@@ -88,10 +89,10 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
* DhSpec spec = DhSpec.ffdhe3072();
|
||||
*
|
||||
* // Generate a key pair using the registered builder
|
||||
* KeyPair kp = CryptoAlgorithms.keyPair("DH", spec);
|
||||
* KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("DH", spec);
|
||||
*
|
||||
* // Obtain an agreement context for DH key agreement
|
||||
* AgreementContext ctx = CryptoAlgorithms.create("DH", KeyUsage.AGREEMENT, kp.getPrivate(), spec);
|
||||
* AgreementContext ctx = session.createContext("DH", KeyUsage.AGREEMENT, kp.getPrivate(), spec);
|
||||
*
|
||||
* // Use the context with a peer public key to derive a shared secret
|
||||
* ctx.setPeerPublic(peerPublicKey);
|
||||
@@ -140,43 +141,28 @@ public final class DhAlgorithm extends AbstractCryptoAlgorithm {
|
||||
"DiffieHellman", null, "DH", null),
|
||||
DhSpec::ffdhe2048);
|
||||
|
||||
registerAsymmetricKeyBuilder(DhSpec.class, new DhKeyGenBuilder(), DhSpec::ffdhe2048);
|
||||
registerAsymmetricKeyBuilder(DhPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
|
||||
@Override
|
||||
public KeyPair generateKeyPair(DhPublicKeySpec spec) throws GeneralSecurityException {
|
||||
throw new UnsupportedOperationException("Use DhKeyGenBuilder for keypair generation.");
|
||||
}
|
||||
registerAsymmetricKeyPairGenerator(DhSpec.class, new DhKeyGenBuilder(), DhSpec::ffdhe2048);
|
||||
registerPublicKeyImporter(DhPublicKeySpec.class, new PublicKeyImporter<>() {
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(DhPublicKeySpec spec) throws GeneralSecurityException {
|
||||
KeyFactory kf = KeyFactory.getInstance("DH");
|
||||
return kf.generatePublic(new X509EncodedKeySpec(spec.encoded()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(DhPublicKeySpec spec) throws GeneralSecurityException {
|
||||
throw new UnsupportedOperationException("Use DhPrivateKeySpec for private key import.");
|
||||
}
|
||||
}, null);
|
||||
registerAsymmetricKeyBuilder(DhPrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
|
||||
@Override
|
||||
public KeyPair generateKeyPair(DhPrivateKeySpec spec) throws GeneralSecurityException {
|
||||
throw new UnsupportedOperationException("Use DhKeyGenBuilder for keypair generation.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(DhPrivateKeySpec spec) throws GeneralSecurityException {
|
||||
throw new UnsupportedOperationException("Use DhPrivateKeySpec for public key import.");
|
||||
}
|
||||
});
|
||||
registerPrivateKeyImporter(DhPrivateKeySpec.class, new PrivateKeyImporter<>() {
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(DhPrivateKeySpec spec) throws GeneralSecurityException {
|
||||
KeyFactory kf = KeyFactory.getInstance("DH");
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded()));
|
||||
byte[] encoded = spec.encoded();
|
||||
try {
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
|
||||
} finally {
|
||||
Arrays.fill(encoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,12 +38,10 @@ import java.security.AlgorithmParameters;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
|
||||
import javax.crypto.spec.DHParameterSpec;
|
||||
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.AsymmetricKeyPairGenerator;
|
||||
|
||||
/**
|
||||
* <h2>DH key pair builder</h2>
|
||||
@@ -89,7 +87,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
* KeyPair kp2 = builder.generateKeyPair(sized);
|
||||
* }</pre>
|
||||
*/
|
||||
public final class DhKeyGenBuilder implements AsymmetricKeyBuilder<DhSpec> {
|
||||
public final class DhKeyGenBuilder implements AsymmetricKeyPairGenerator<DhSpec> {
|
||||
/**
|
||||
* Generates a Diffie-Hellman key pair for the given specification.
|
||||
*
|
||||
@@ -138,56 +136,4 @@ public final class DhKeyGenBuilder implements AsymmetricKeyBuilder<DhSpec> {
|
||||
kpg.initialize(dh);
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported for DH in this builder.
|
||||
*
|
||||
* <p>
|
||||
* Raw public key import is not implemented because this builder focuses on key
|
||||
* generation from DH parameters. Use higher-level catalog or codec facilities
|
||||
* to parse or construct {@link PublicKey} instances if needed.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* <strong>Example</strong>
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
* // This will throw UnsupportedOperationException
|
||||
* new DhKeyGenBuilder().importPublic(DhSpec.ffdhe2048());
|
||||
* }</pre>
|
||||
*
|
||||
* @param spec the DH specification (ignored)
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always thrown
|
||||
*/
|
||||
@Override
|
||||
public PublicKey importPublic(DhSpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported for DH in this builder.
|
||||
*
|
||||
* <p>
|
||||
* Raw private key import is not implemented because this builder focuses on key
|
||||
* generation from DH parameters. Use higher-level catalog or codec facilities
|
||||
* to parse or construct {@link PrivateKey} instances if needed.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* <strong>Example</strong>
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
* // This will throw UnsupportedOperationException
|
||||
* new DhKeyGenBuilder().importPrivate(DhSpec.ffdhe2048());
|
||||
* }</pre>
|
||||
*
|
||||
* @param spec the DH specification (ignored)
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always thrown
|
||||
*/
|
||||
@Override
|
||||
public PrivateKey importPrivate(DhSpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,11 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.dh;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
@@ -50,8 +54,9 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* <h2>Design</h2>
|
||||
* <ul>
|
||||
* <li>Immutable: the internal byte array is defensively copied at construction
|
||||
* and when returned by {@link #encoded()}.</li>
|
||||
* <li>Destroyable: the internal byte array is defensively copied at
|
||||
* construction and when returned by {@link #encoded()}, and is cleared by
|
||||
* {@link #destroy()}.</li>
|
||||
* <li>Encodable: supports marshaling to/from a
|
||||
* {@link zeroecho.core.marshal.PairSeq} so keys can be serialized in
|
||||
* human-readable or protocol-friendly formats.</li>
|
||||
@@ -62,7 +67,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* <h2>Example</h2> <pre>{@code
|
||||
* // Import a DH private key from encoded bytes
|
||||
* DhPrivateKeySpec spec = new DhPrivateKeySpec(pkcs8Bytes);
|
||||
* PrivateKey priv = CryptoAlgorithms.privateKey("DH", spec);
|
||||
* PrivateKey priv = session.keyBuilders().asymmetric().importPrivate("DH", spec);
|
||||
*
|
||||
* // Marshal to a text-friendly representation
|
||||
* PairSeq ps = DhPrivateKeySpec.marshal(spec);
|
||||
@@ -73,10 +78,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public class DhPrivateKeySpec implements AlgorithmKeySpec {
|
||||
public class DhPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
|
||||
|
||||
private static final String PKCS8_B64 = "pkcs8.b64";
|
||||
private final byte[] pkcs8;
|
||||
private final ReentrantLock lifecycleLock = new ReentrantLock();
|
||||
private boolean destroyed;
|
||||
|
||||
/**
|
||||
* Creates a new specification from a PKCS#8 encoded DH private key.
|
||||
@@ -97,7 +104,13 @@ public class DhPrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return a defensive copy of the PKCS#8 encoded DH private key
|
||||
*/
|
||||
public byte[] encoded() {
|
||||
return pkcs8.clone();
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return pkcs8.clone();
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,7 +126,7 @@ public class DhPrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @throws NullPointerException if {@code spec} is {@code null}
|
||||
*/
|
||||
public static PairSeq marshal(DhPrivateKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
|
||||
String b64 = spec.encodedKey();
|
||||
return PairSeq.of("type", "DH-PRIV", PKCS8_B64, b64);
|
||||
}
|
||||
|
||||
@@ -137,12 +150,62 @@ public class DhPrivateKeySpec implements AlgorithmKeySpec {
|
||||
String k = cur.key();
|
||||
String v = cur.value();
|
||||
if (PKCS8_B64.equals(k)) {
|
||||
out = Base64.getDecoder().decode(v);
|
||||
out = decodeReplacing(out, v);
|
||||
}
|
||||
}
|
||||
if (out == null) {
|
||||
throw new IllegalArgumentException("pkcs8.b64 missing for DH private key");
|
||||
}
|
||||
return new DhPrivateKeySpec(out);
|
||||
try {
|
||||
return new DhPrivateKeySpec(out);
|
||||
} finally {
|
||||
Arrays.fill(out, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] decodeReplacing(byte[] current, String encoded) {
|
||||
if (current != null) {
|
||||
Arrays.fill(current, (byte) 0);
|
||||
}
|
||||
return Base64.getDecoder().decode(encoded);
|
||||
}
|
||||
|
||||
private String encodedKey() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
if (!destroyed) {
|
||||
Arrays.fill(pkcs8, (byte) 0);
|
||||
destroyed = true;
|
||||
}
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
return destroyed;
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureActive() {
|
||||
if (destroyed) {
|
||||
throw new IllegalStateException("DH private key specification has been destroyed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* <h2>Example</h2> <pre>{@code
|
||||
* // Import a DH public key from encoded bytes
|
||||
* DhPublicKeySpec spec = new DhPublicKeySpec(x509Bytes);
|
||||
* PublicKey pub = CryptoAlgorithms.publicKey("DH", spec);
|
||||
* PublicKey pub = session.keyBuilders().asymmetric().importPublic("DH", spec);
|
||||
*
|
||||
* // Marshal to a text-friendly representation
|
||||
* PairSeq ps = DhPublicKeySpec.marshal(spec);
|
||||
|
||||
@@ -80,10 +80,10 @@ import zeroecho.core.spec.ContextSpec;
|
||||
*
|
||||
* <h2>Example</h2> <pre>{@code
|
||||
* // Create a key pair in the FFDHE-3072 group
|
||||
* KeyPair kp = CryptoAlgorithms.keyPair("DH", DhSpec.ffdhe3072());
|
||||
* KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("DH", DhSpec.ffdhe3072());
|
||||
*
|
||||
* // Establish an agreement context
|
||||
* AgreementContext ctx = CryptoAlgorithms.create(
|
||||
* AgreementContext ctx = session.createContext(
|
||||
* "DH", KeyUsage.AGREEMENT, kp.getPrivate(), DhSpec.ffdhe3072());
|
||||
* }</pre>
|
||||
*
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
* ad-hoc parameter generation.</li>
|
||||
* <li>Expose predefined RFC 7919 FFDHE groups for safe parameter selection.
|
||||
* </li>
|
||||
* <li>Allow import/export of encoded keys via immutable key specs supporting
|
||||
* PKCS#8 and X.509.</li>
|
||||
* <li>Allow import/export of encoded keys via defensively copying key specs
|
||||
* supporting PKCS#8 and X.509; private-key specs are destroyable.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Components</h2>
|
||||
@@ -65,9 +65,10 @@
|
||||
* {@link javax.crypto.spec.DHParameterSpec} instances.</li>
|
||||
* <li><b>DhSpec</b>: immutable container for DH parameters; provides static
|
||||
* factories for FFDHE groups (2048–8192 bits).</li>
|
||||
* <li><b>DhPublicKeySpec</b> and <b>DhPrivateKeySpec</b>: immutable encoded key
|
||||
* specs for importing/exporting X.509 and PKCS#8 encodings, with
|
||||
* {@link zeroecho.core.marshal.PairSeq} marshalling support.</li>
|
||||
* <li><b>DhPublicKeySpec</b> and <b>DhPrivateKeySpec</b>: encoded key specs for
|
||||
* importing/exporting X.509 and PKCS#8 encodings, with
|
||||
* {@link zeroecho.core.marshal.PairSeq} marshalling support; the private-key
|
||||
* form is destroyable.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Design notes</h2>
|
||||
|
||||
@@ -76,7 +76,7 @@ import zeroecho.core.spec.ContextSpec;
|
||||
* DigestSpec spec = DigestSpec.shake256(64);
|
||||
*
|
||||
* // Use in context creation
|
||||
* DigestContext ctx = CryptoAlgorithms.create(
|
||||
* DigestContext ctx = session.createContext(
|
||||
* "DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, spec);
|
||||
*
|
||||
* byte[] digest = ctx.doFinal(data);
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.digest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
@@ -43,6 +42,7 @@ import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.NullKey;
|
||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||
import zeroecho.core.context.DigestContext;
|
||||
import zeroecho.core.err.ProviderFailureException;
|
||||
|
||||
/**
|
||||
* <h2>SHA-2, SHA-3, and SHAKE digest algorithms</h2>
|
||||
@@ -77,7 +77,7 @@ import zeroecho.core.context.DigestContext;
|
||||
* CryptoAlgorithm algo = CryptoAlgorithms.require("DIGEST");
|
||||
*
|
||||
* // Create a digest context for SHA3-512
|
||||
* DigestContext ctx = CryptoAlgorithms.create(
|
||||
* DigestContext ctx = session.createContext(
|
||||
* "DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, DigestSpec.sha3_512());
|
||||
*
|
||||
* // Stream data into the digest
|
||||
@@ -117,7 +117,8 @@ public final class Sha2Sha3Algorithm extends AbstractCryptoAlgorithm {
|
||||
MessageDigest md = MessageDigest.getInstance(s.algorithm().jca());
|
||||
return new JcaDigestContext(this, md, s);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IOException("Failed to init MessageDigest: " + s.algorithm().jca(), e);
|
||||
throw new ProviderFailureException(
|
||||
"Failed to initialize MessageDigest " + s.algorithm().jca(), e);
|
||||
}
|
||||
}, DigestSpec::sha256 // default for catalog/tests
|
||||
);
|
||||
|
||||
@@ -90,18 +90,18 @@ import zeroecho.core.context.MessageAgreementContext;
|
||||
*
|
||||
* <h2>Example</h2> <pre>{@code
|
||||
* // Generate a key pair for Alice
|
||||
* KeyPair aliceKeys = CryptoAlgorithms.generateKeyPair("ECDH", EcdhCurveSpec.P256);
|
||||
* KeyPair aliceKeys = session.keyBuilders().asymmetric().generateKeyPair("ECDH", EcdhCurveSpec.P256);
|
||||
*
|
||||
* // Generate a key pair for Bob
|
||||
* KeyPair bobKeys = CryptoAlgorithms.generateKeyPair("ECDH", EcdhCurveSpec.P256);
|
||||
* KeyPair bobKeys = session.keyBuilders().asymmetric().generateKeyPair("ECDH", EcdhCurveSpec.P256);
|
||||
*
|
||||
* // Alice computes shared secret using her private key
|
||||
* AgreementContext aliceCtx = CryptoAlgorithms.create("ECDH",
|
||||
* AgreementContext aliceCtx = session.createContext("ECDH",
|
||||
* KeyUsage.AGREEMENT, aliceKeys.getPrivate(), EcdsaCurveSpec.P256);
|
||||
* byte[] aliceSecret = aliceCtx.derive(bobKeys.getPublic());
|
||||
*
|
||||
* // Bob computes shared secret using his private key
|
||||
* AgreementContext bobCtx = CryptoAlgorithms.create("ECDH",
|
||||
* AgreementContext bobCtx = session.createContext("ECDH",
|
||||
* KeyUsage.AGREEMENT, bobKeys.getPrivate(), EcdsaCurveSpec.P256);
|
||||
* byte[] bobSecret = bobCtx.derive(aliceKeys.getPublic());
|
||||
*
|
||||
@@ -160,8 +160,9 @@ public final class EcdhAlgorithm extends AbstractCryptoAlgorithm {
|
||||
() -> EcdsaCurveSpec.P256);
|
||||
|
||||
// Reuse EC builders/importers
|
||||
registerAsymmetricKeyBuilder(EcdhCurveSpec.class, new EcdhKeyGenBuilder(), () -> EcdhCurveSpec.P256);
|
||||
registerAsymmetricKeyBuilder(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder(), null);
|
||||
registerAsymmetricKeyBuilder(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder(), null);
|
||||
registerAsymmetricKeyPairGenerator(EcdhCurveSpec.class, new EcdhKeyGenBuilder(),
|
||||
() -> EcdhCurveSpec.P256);
|
||||
registerPublicKeyImporter(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder());
|
||||
registerPrivateKeyImporter(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ import zeroecho.core.spec.ContextSpec;
|
||||
*
|
||||
* <h2>Usage</h2> <pre>{@code
|
||||
* // Generate a key pair on P-256
|
||||
* KeyPair kp = CryptoAlgorithms.generateKeyPair("ECDH", EcdhCurveSpec.P256);
|
||||
* KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("ECDH", EcdhCurveSpec.P256);
|
||||
*
|
||||
* // Use curve metadata
|
||||
* String jcaName = EcdhCurveSpec.P256.curveName(); // "secp256r1"
|
||||
|
||||
@@ -39,15 +39,13 @@ import java.security.KeyPairGenerator;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
|
||||
import zeroecho.core.alg.ecdsa.EcdsaPrivateKeyBuilder;
|
||||
import zeroecho.core.alg.ecdsa.EcdsaPrivateKeySpec;
|
||||
import zeroecho.core.alg.ecdsa.EcdsaPublicKeyBuilder;
|
||||
import zeroecho.core.alg.ecdsa.EcdsaPublicKeySpec;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.AsymmetricKeyPairGenerator;
|
||||
|
||||
/**
|
||||
* <h2>ECDH Key Pair Generator</h2>
|
||||
*
|
||||
* Implementation of {@link AsymmetricKeyBuilder} for elliptic curve
|
||||
* Implementation of {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} for elliptic curve
|
||||
* Diffie-Hellman (ECDH) key pairs.
|
||||
*
|
||||
* <p>
|
||||
@@ -70,7 +68,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class EcdhKeyGenBuilder implements AsymmetricKeyBuilder<EcdhCurveSpec> {
|
||||
public final class EcdhKeyGenBuilder implements AsymmetricKeyPairGenerator<EcdhCurveSpec> {
|
||||
/**
|
||||
* Generates a new elliptic curve key pair for use in ECDH key agreement.
|
||||
*
|
||||
@@ -92,40 +90,4 @@ public final class EcdhKeyGenBuilder implements AsymmetricKeyBuilder<EcdhCurveSp
|
||||
kpg.initialize(new ECGenParameterSpec(spec.curveName()));
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported operation for this builder.
|
||||
*
|
||||
* <p>
|
||||
* Importing existing ECDH public keys should be performed via
|
||||
* {@link EcdsaPublicKeyBuilder} with an {@link EcdsaPublicKeySpec}. This method
|
||||
* will always throw an {@link UnsupportedOperationException}.
|
||||
* </p>
|
||||
*
|
||||
* @param spec ignored
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always
|
||||
*/
|
||||
@Override
|
||||
public java.security.PublicKey importPublic(EcdhCurveSpec spec) {
|
||||
throw new UnsupportedOperationException("Use EcdhPublicKeySpec with EcdsaPublicKeyBuilder.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported operation for this builder.
|
||||
*
|
||||
* <p>
|
||||
* Importing existing ECDH private keys should be performed via
|
||||
* {@link EcdsaPrivateKeyBuilder} with an {@link EcdsaPrivateKeySpec}. This
|
||||
* method will always throw an {@link UnsupportedOperationException}.
|
||||
* </p>
|
||||
*
|
||||
* @param spec ignored
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always
|
||||
*/
|
||||
@Override
|
||||
public java.security.PrivateKey importPrivate(EcdhCurveSpec spec) {
|
||||
throw new UnsupportedOperationException("Use EcdhPrivateKeySpec with EcdsaPrivateKeyBuilder.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ import java.security.PublicKey;
|
||||
|
||||
import zeroecho.core.AlgorithmFamily;
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.CryptoCatalog;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||
@@ -82,9 +81,9 @@ import zeroecho.core.context.SignatureContext;
|
||||
*
|
||||
* <pre>{@code
|
||||
* // Example: Sign and verify with ECDSA/P-256
|
||||
* KeyPair kp = CryptoAlgorithms.keyPair("ECDSA", EcdsaCurveSpec.P256);
|
||||
* SignatureContext signer = CryptoAlgorithms.create("ECDSA", KeyUsage.SIGN, kp.getPrivate(), EcdsaCurveSpec.P256);
|
||||
* SignatureContext verifier = CryptoAlgorithms.create("ECDSA", KeyUsage.VERIFY, kp.getPublic(), EcdsaCurveSpec.P256);
|
||||
* KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("ECDSA", EcdsaCurveSpec.P256);
|
||||
* SignatureContext signer = session.createContext("ECDSA", KeyUsage.SIGN, kp.getPrivate(), EcdsaCurveSpec.P256);
|
||||
* SignatureContext verifier = session.createContext("ECDSA", KeyUsage.VERIFY, kp.getPublic(), EcdsaCurveSpec.P256);
|
||||
* }</pre>
|
||||
*
|
||||
* @since 1.0
|
||||
@@ -104,8 +103,8 @@ public final class EcdsaAlgorithm extends AbstractCryptoAlgorithm {
|
||||
* <p>
|
||||
* On construction, the algorithm declares its supported roles and registers
|
||||
* builders with the {@link CryptoAlgorithm} infrastructure so they can be
|
||||
* discovered by the {@link CryptoCatalog} or invoked through
|
||||
* {@link CryptoAlgorithms} convenience methods.
|
||||
* discovered by the {@link CryptoCatalog} or invoked through the
|
||||
* session-bound {@link zeroecho.sdk.KeyBuilders} entry point.
|
||||
* </p>
|
||||
*/
|
||||
public EcdsaAlgorithm() {
|
||||
@@ -135,8 +134,9 @@ public final class EcdsaAlgorithm extends AbstractCryptoAlgorithm {
|
||||
}
|
||||
}, () -> EcdsaCurveSpec.P256);
|
||||
|
||||
registerAsymmetricKeyBuilder(EcdsaCurveSpec.class, new EcdsaKeyGenBuilder(), () -> EcdsaCurveSpec.P256);
|
||||
registerAsymmetricKeyBuilder(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder(), null);
|
||||
registerAsymmetricKeyBuilder(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder(), null);
|
||||
registerAsymmetricKeyPairGenerator(EcdsaCurveSpec.class, new EcdsaKeyGenBuilder(),
|
||||
() -> EcdsaCurveSpec.P256);
|
||||
registerPublicKeyImporter(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder());
|
||||
registerPrivateKeyImporter(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,30 +39,23 @@ import java.security.KeyPairGenerator;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.AsymmetricKeyPairGenerator;
|
||||
|
||||
/**
|
||||
* <h2>ECDSA Key Pair Generator</h2>
|
||||
*
|
||||
* Implementation of {@link AsymmetricKeyBuilder} for {@link EcdsaCurveSpec}.
|
||||
* Implementation of {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} for
|
||||
* {@link EcdsaCurveSpec}.
|
||||
* This builder is responsible for generating new elliptic curve key pairs for
|
||||
* use with the {@link EcdsaAlgorithm}.
|
||||
*
|
||||
* <h2>Supported operations</h2>
|
||||
* <ul>
|
||||
* <li>{@link #generateKeyPair(EcdsaCurveSpec)} - create a fresh key pair for
|
||||
* the given named curve.</li>
|
||||
* <li>{@link #importPublic(EcdsaCurveSpec)} - unsupported; use
|
||||
* {@link EcdsaPublicKeySpec} with {@link EcdsaPublicKeyBuilder} instead.</li>
|
||||
* <li>{@link #importPrivate(EcdsaCurveSpec)} - unsupported; use
|
||||
* {@link EcdsaPrivateKeySpec} with {@link EcdsaPrivateKeyBuilder} instead.</li>
|
||||
* </ul>
|
||||
* <p>The exact supported operation is
|
||||
* {@link #generateKeyPair(EcdsaCurveSpec)}. Public and private import are
|
||||
* registered separately through {@link EcdsaPublicKeyBuilder} and
|
||||
* {@link EcdsaPrivateKeyBuilder}.</p>
|
||||
*
|
||||
* <h2>Usage</h2> Typically accessed indirectly through
|
||||
* {@link CryptoAlgorithms#keyPair(String, zeroecho.core.spec.AlgorithmKeySpec)}
|
||||
* or
|
||||
* {@link CryptoAlgorithm#generateKeyPair(zeroecho.core.spec.AlgorithmKeySpec)}.
|
||||
* <h2>Usage</h2> Typically accessed through the session key-operation API or
|
||||
* {@link CryptoAlgorithm#asymmetricKeyPairGenerator(Class)}.
|
||||
*
|
||||
* <pre>{@code
|
||||
* // Example: Generate an ECDSA P-256 key pair
|
||||
@@ -72,7 +65,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class EcdsaKeyGenBuilder implements AsymmetricKeyBuilder<EcdsaCurveSpec> {
|
||||
public final class EcdsaKeyGenBuilder implements AsymmetricKeyPairGenerator<EcdsaCurveSpec> {
|
||||
/**
|
||||
* Generates a new elliptic curve key pair for the given curve specification.
|
||||
*
|
||||
@@ -93,38 +86,4 @@ public final class EcdsaKeyGenBuilder implements AsymmetricKeyBuilder<EcdsaCurve
|
||||
kpg.initialize(new ECGenParameterSpec(spec.curveName()));
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported operation for this builder.
|
||||
*
|
||||
* <p>
|
||||
* Public key import should be performed using {@link EcdsaPublicKeySpec} and
|
||||
* {@link EcdsaPublicKeyBuilder}.
|
||||
* </p>
|
||||
*
|
||||
* @param spec unused curve specification
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always thrown
|
||||
*/
|
||||
@Override
|
||||
public java.security.PublicKey importPublic(EcdsaCurveSpec spec) {
|
||||
throw new UnsupportedOperationException("Use EcdsaPublicKeySpec with EcdsaPublicKeyBuilder.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported operation for this builder.
|
||||
*
|
||||
* <p>
|
||||
* Private key import should be performed using {@link EcdsaPrivateKeySpec} and
|
||||
* {@link EcdsaPrivateKeyBuilder}.
|
||||
* </p>
|
||||
*
|
||||
* @param spec unused curve specification
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always thrown
|
||||
*/
|
||||
@Override
|
||||
public java.security.PrivateKey importPrivate(EcdsaCurveSpec spec) {
|
||||
throw new UnsupportedOperationException("Use EcdsaPrivateKeySpec with EcdsaPrivateKeyBuilder.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,36 +37,28 @@ import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.PrivateKeyImporter;
|
||||
|
||||
/**
|
||||
* <h2>ECDSA Private Key Builder</h2>
|
||||
*
|
||||
* Implementation of {@link AsymmetricKeyBuilder} for
|
||||
* Implementation of {@link zeroecho.core.spi.PrivateKeyImporter} for
|
||||
* {@link EcdsaPrivateKeySpec}. This builder is responsible for importing ECDSA
|
||||
* private keys from encoded representations.
|
||||
*
|
||||
* <h2>Supported operations</h2>
|
||||
* <ul>
|
||||
* <li>{@link #importPrivate(EcdsaPrivateKeySpec)} - construct a
|
||||
* {@link PrivateKey} instance from a PKCS#8 encoded key.</li>
|
||||
* <li>{@link #generateKeyPair(EcdsaPrivateKeySpec)} - unsupported; use
|
||||
* {@link EcdsaKeyGenBuilder} instead.</li>
|
||||
* <li>{@link #importPublic(EcdsaPrivateKeySpec)} - unsupported; use
|
||||
* {@link EcdsaPublicKeySpec} with {@link EcdsaPublicKeyBuilder} instead.</li>
|
||||
* </ul>
|
||||
* <p>The exact supported operation is
|
||||
* {@link #importPrivate(EcdsaPrivateKeySpec)}. Generation and public import are
|
||||
* registered through their own operation-specific implementations.</p>
|
||||
*
|
||||
* <h2>Encoding</h2> The {@link EcdsaPrivateKeySpec} stores the private key in
|
||||
* PKCS#8 DER format. This builder delegates to a JCA {@link KeyFactory} for the
|
||||
* {@code "EC"} algorithm to reconstruct a usable {@link PrivateKey}.
|
||||
*
|
||||
* <h2>Usage</h2> Typically accessed indirectly through
|
||||
* {@link CryptoAlgorithms#privateKey(String, zeroecho.core.spec.AlgorithmKeySpec)}
|
||||
* or
|
||||
* {@link CryptoAlgorithm#importPrivate(zeroecho.core.spec.AlgorithmKeySpec)}.
|
||||
* <h2>Usage</h2> Typically accessed through the session key-operation API or
|
||||
* {@link CryptoAlgorithm#privateKeyImporter(Class)}.
|
||||
*
|
||||
* <pre>{@code
|
||||
* // Example: Import an ECDSA private key
|
||||
@@ -77,40 +69,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class EcdsaPrivateKeyBuilder implements AsymmetricKeyBuilder<EcdsaPrivateKeySpec> {
|
||||
/**
|
||||
* Unsupported operation for this builder.
|
||||
*
|
||||
* <p>
|
||||
* ECDSA key pair generation should be performed using
|
||||
* {@link EcdsaKeyGenBuilder}, not from a private key specification.
|
||||
* </p>
|
||||
*
|
||||
* @param spec unused private key specification
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always thrown
|
||||
*/
|
||||
@Override
|
||||
public java.security.KeyPair generateKeyPair(EcdsaPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use EcdsaKeyGenBuilder for keypair generation.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported operation for this builder.
|
||||
*
|
||||
* <p>
|
||||
* Public key import should be performed using {@link EcdsaPublicKeySpec} with
|
||||
* {@link EcdsaPublicKeyBuilder}.
|
||||
* </p>
|
||||
*
|
||||
* @param spec unused private key specification
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always thrown
|
||||
*/
|
||||
@Override
|
||||
public java.security.PublicKey importPublic(EcdsaPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use EcdsaPublicKeySpec with EcdsaPublicKeyBuilder.");
|
||||
}
|
||||
public final class EcdsaPrivateKeyBuilder implements PrivateKeyImporter<EcdsaPrivateKeySpec> {
|
||||
|
||||
/**
|
||||
* Imports a private key from a PKCS#8 encoded specification.
|
||||
@@ -128,6 +87,11 @@ public final class EcdsaPrivateKeyBuilder implements AsymmetricKeyBuilder<EcdsaP
|
||||
@Override
|
||||
public PrivateKey importPrivate(EcdsaPrivateKeySpec spec) throws GeneralSecurityException {
|
||||
KeyFactory kf = KeyFactory.getInstance("EC");
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded()));
|
||||
byte[] encoded = spec.encoded();
|
||||
try {
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
|
||||
} finally {
|
||||
Arrays.fill(encoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,11 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.ecdsa;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
@@ -41,7 +45,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
/**
|
||||
* <h2>ECDSA Private Key Specification</h2>
|
||||
*
|
||||
* An immutable wrapper around a PKCS#8-encoded ECDSA private key. This
|
||||
* A destroyable wrapper around a PKCS#8-encoded ECDSA private key. This
|
||||
* specification is used by {@link EcdsaPrivateKeyBuilder} to import keys into
|
||||
* the JCA {@link java.security.PrivateKey} representation.
|
||||
*
|
||||
@@ -69,10 +73,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec {
|
||||
public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
|
||||
|
||||
private static final String PKCS8_B64 = "pkcs8.b64";
|
||||
private final byte[] pkcs8;
|
||||
private final ReentrantLock lifecycleLock = new ReentrantLock();
|
||||
private boolean destroyed;
|
||||
|
||||
/**
|
||||
* Creates a new private key specification from a PKCS#8 encoded byte array.
|
||||
@@ -93,7 +99,13 @@ public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return cloned PKCS#8 byte array
|
||||
*/
|
||||
public byte[] encoded() {
|
||||
return pkcs8.clone();
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return pkcs8.clone();
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,7 +120,7 @@ public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return serialized representation in key-value form
|
||||
*/
|
||||
public static PairSeq marshal(EcdsaPrivateKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
|
||||
String b64 = spec.encodedKey();
|
||||
return PairSeq.of("type", "ECDSA-PRIV", PKCS8_B64, b64);
|
||||
}
|
||||
|
||||
@@ -131,12 +143,62 @@ public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec {
|
||||
String k = cur.key();
|
||||
String v = cur.value();
|
||||
if (PKCS8_B64.equals(k)) {
|
||||
out = Base64.getDecoder().decode(v);
|
||||
out = decodeReplacing(out, v);
|
||||
}
|
||||
}
|
||||
if (out == null) {
|
||||
throw new IllegalArgumentException("pkcs8.b64 missing for ECDSA private key");
|
||||
}
|
||||
return new EcdsaPrivateKeySpec(out);
|
||||
try {
|
||||
return new EcdsaPrivateKeySpec(out);
|
||||
} finally {
|
||||
Arrays.fill(out, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] decodeReplacing(byte[] current, String encoded) {
|
||||
if (current != null) {
|
||||
Arrays.fill(current, (byte) 0);
|
||||
}
|
||||
return Base64.getDecoder().decode(encoded);
|
||||
}
|
||||
|
||||
private String encodedKey() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
if (!destroyed) {
|
||||
Arrays.fill(pkcs8, (byte) 0);
|
||||
destroyed = true;
|
||||
}
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
return destroyed;
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureActive() {
|
||||
if (destroyed) {
|
||||
throw new IllegalStateException("ECDSA private key specification has been destroyed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,33 +39,25 @@ import java.security.PublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.PublicKeyImporter;
|
||||
|
||||
/**
|
||||
* <h2>ECDSA Public Key Builder</h2>
|
||||
*
|
||||
* Implementation of {@link AsymmetricKeyBuilder} for
|
||||
* Implementation of {@link zeroecho.core.spi.PublicKeyImporter} for
|
||||
* {@link EcdsaPublicKeySpec}. This builder is responsible for importing ECDSA
|
||||
* public keys from X.509 SubjectPublicKeyInfo encodings.
|
||||
*
|
||||
* <h2>Supported operations</h2>
|
||||
* <ul>
|
||||
* <li>{@link #importPublic(EcdsaPublicKeySpec)} - construct a {@link PublicKey}
|
||||
* instance from an X.509-encoded key.</li>
|
||||
* <li>{@link #generateKeyPair(EcdsaPublicKeySpec)} - unsupported; use
|
||||
* {@link EcdsaKeyGenBuilder} instead.</li>
|
||||
* <li>{@link #importPrivate(EcdsaPublicKeySpec)} - unsupported; use
|
||||
* {@link EcdsaPrivateKeySpec} with {@link EcdsaPrivateKeyBuilder} instead.</li>
|
||||
* </ul>
|
||||
* <p>The exact supported operation is
|
||||
* {@link #importPublic(EcdsaPublicKeySpec)}. Generation and private import are
|
||||
* registered through their own operation-specific implementations.</p>
|
||||
*
|
||||
* <h2>Encoding</h2> The {@link EcdsaPublicKeySpec} stores the public key in
|
||||
* standard X.509 DER format. This builder delegates to a JCA {@link KeyFactory}
|
||||
* for the {@code "EC"} algorithm to reconstruct a usable {@link PublicKey}.
|
||||
*
|
||||
* <h2>Usage</h2> Typically accessed indirectly through
|
||||
* {@link CryptoAlgorithms#publicKey(String, zeroecho.core.spec.AlgorithmKeySpec)}
|
||||
* or {@link CryptoAlgorithm#importPublic(zeroecho.core.spec.AlgorithmKeySpec)}.
|
||||
* <h2>Usage</h2> Typically accessed through the session key-operation API or
|
||||
* {@link CryptoAlgorithm#publicKeyImporter(Class)}.
|
||||
*
|
||||
* <pre>{@code
|
||||
* // Example: Import an ECDSA public key
|
||||
@@ -76,23 +68,7 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class EcdsaPublicKeyBuilder implements AsymmetricKeyBuilder<EcdsaPublicKeySpec> {
|
||||
/**
|
||||
* Unsupported operation for this builder.
|
||||
*
|
||||
* <p>
|
||||
* ECDSA key pair generation should be performed using
|
||||
* {@link EcdsaKeyGenBuilder}, not from a public key specification.
|
||||
* </p>
|
||||
*
|
||||
* @param spec unused public key specification
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always thrown
|
||||
*/
|
||||
@Override
|
||||
public java.security.KeyPair generateKeyPair(EcdsaPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use EcdsaKeyGenBuilder for keypair generation.");
|
||||
}
|
||||
public final class EcdsaPublicKeyBuilder implements PublicKeyImporter<EcdsaPublicKeySpec> {
|
||||
|
||||
/**
|
||||
* Imports a public key from an X.509 SubjectPublicKeyInfo specification.
|
||||
@@ -112,21 +88,4 @@ public final class EcdsaPublicKeyBuilder implements AsymmetricKeyBuilder<EcdsaPu
|
||||
KeyFactory kf = KeyFactory.getInstance("EC");
|
||||
return kf.generatePublic(new X509EncodedKeySpec(spec.encoded()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported operation for this builder.
|
||||
*
|
||||
* <p>
|
||||
* Private key import should be performed using {@link EcdsaPrivateKeySpec} with
|
||||
* {@link EcdsaPrivateKeyBuilder}.
|
||||
* </p>
|
||||
*
|
||||
* @param spec unused public key specification
|
||||
* @return never returns normally
|
||||
* @throws UnsupportedOperationException always thrown
|
||||
*/
|
||||
@Override
|
||||
public java.security.PrivateKey importPrivate(EcdsaPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use EcdsaPrivateKeySpec with EcdsaPrivateKeyBuilder.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@
|
||||
*
|
||||
* <p>
|
||||
* This package provides the ECDSA algorithm descriptor, curve specifications,
|
||||
* key builders for generation and import, and immutable encoded key specs. It
|
||||
* wires ECDSA into the core signature SPI through a JCA-backed streaming
|
||||
* signature context that enforces fixed signature lengths.
|
||||
* key builders for generation and import, and defensively copying encoded key
|
||||
* specs. It wires ECDSA into the core signature SPI through a JCA-backed
|
||||
* streaming signature context that enforces fixed signature lengths.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Scope and responsibilities</h2>
|
||||
@@ -66,8 +66,9 @@
|
||||
* <li><b>EcdsaPublicKeyBuilder</b> and <b>EcdsaPrivateKeyBuilder:</b> import
|
||||
* keys from X.509 and PKCS#8 encodings via
|
||||
* {@link java.security.KeyFactory}.</li>
|
||||
* <li><b>EcdsaPublicKeySpec</b> and <b>EcdsaPrivateKeySpec:</b> immutable
|
||||
* wrappers around encoded keys with marshalling support.</li>
|
||||
* <li><b>EcdsaPublicKeySpec</b> and <b>EcdsaPrivateKeySpec:</b> wrappers around
|
||||
* encoded keys with marshalling support; the private-key form is
|
||||
* destroyable.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Design notes</h2>
|
||||
|
||||
@@ -122,9 +122,9 @@ public final class Ed25519Algorithm extends AbstractCryptoAlgorithm {
|
||||
}, () -> VoidSpec.INSTANCE);
|
||||
|
||||
// Key builders
|
||||
registerAsymmetricKeyBuilder(Ed25519KeyGenSpec.class, new Ed25519KeyGenBuilder(),
|
||||
registerAsymmetricKeyPairGenerator(Ed25519KeyGenSpec.class, new Ed25519KeyGenBuilder(),
|
||||
Ed25519KeyGenSpec::defaultSpec);
|
||||
registerAsymmetricKeyBuilder(Ed25519PublicKeySpec.class, new Ed25519PublicKeyBuilder(), null);
|
||||
registerAsymmetricKeyBuilder(Ed25519PrivateKeySpec.class, new Ed25519PrivateKeyBuilder(), null);
|
||||
registerPublicKeyImporter(Ed25519PublicKeySpec.class, new Ed25519PublicKeyBuilder());
|
||||
registerPrivateKeyImporter(Ed25519PrivateKeySpec.class, new Ed25519PrivateKeyBuilder());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEdDSAKeyGenBuilder;
|
||||
/**
|
||||
* <h2>Key-pair builder for Ed25519</h2>
|
||||
*
|
||||
* Concrete {@link zeroecho.core.spi.AsymmetricKeyBuilder} implementation for
|
||||
* Concrete {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} implementation for
|
||||
* generating Ed25519 key pairs.
|
||||
*
|
||||
* <p>
|
||||
@@ -50,7 +50,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEdDSAKeyGenBuilder;
|
||||
* <h2>Usage example</h2> <pre>{@code
|
||||
* // Generate a new Ed25519 key pair with default parameters
|
||||
* Ed25519KeyGenSpec spec = Ed25519KeyGenSpec.defaultSpec();
|
||||
* KeyPair kp = CryptoAlgorithms.keyPair("Ed25519", spec);
|
||||
* KeyPair kp = session.keyBuilders().asymmetric().generateKeyPair("Ed25519", spec);
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Thread-safety</h2> Instances of this builder are stateless and may be
|
||||
|
||||
@@ -50,7 +50,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* <h2>Usage example</h2> <pre>{@code
|
||||
* // Generate a new Ed25519 key pair using the default spec
|
||||
* KeyPair kp = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
|
||||
* KeyPair kp = session.keyBuilders().asymmetric()
|
||||
* .generateKeyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Thread-safety</h2> The default spec instance is immutable and safe to
|
||||
|
||||
@@ -38,7 +38,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder;
|
||||
/**
|
||||
* <h2>Private key builder for Ed25519</h2>
|
||||
*
|
||||
* Concrete {@link zeroecho.core.spi.AsymmetricKeyBuilder} for importing and
|
||||
* Concrete {@link zeroecho.core.spi.PrivateKeyImporter} for importing
|
||||
* wrapping Ed25519 private keys.
|
||||
*
|
||||
* <p>
|
||||
@@ -59,7 +59,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder;
|
||||
* <h2>Usage example</h2> <pre>{@code
|
||||
* // Import a private key from its encoded PKCS#8 form
|
||||
* Ed25519PrivateKeySpec spec = new Ed25519PrivateKeySpec(pkcs8Bytes);
|
||||
* PrivateKey privateKey = CryptoAlgorithms.privateKey("Ed25519", spec);
|
||||
* PrivateKey privateKey = session.keyBuilders().asymmetric().importPrivate("Ed25519", spec);
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Thread-safety</h2> Instances of this builder are stateless and may be
|
||||
|
||||
@@ -33,7 +33,11 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.ed25519;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
@@ -65,7 +69,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* Ed25519PrivateKeySpec spec = new Ed25519PrivateKeySpec(pkcs8Bytes);
|
||||
*
|
||||
* // Import into a PrivateKey using ZeroEcho
|
||||
* PrivateKey priv = CryptoAlgorithms.privateKey("Ed25519", spec);
|
||||
* PrivateKey priv = session.keyBuilders().asymmetric().importPrivate("Ed25519", spec);
|
||||
*
|
||||
* // Serialize to PairSeq (e.g., for configuration or transport)
|
||||
* PairSeq seq = Ed25519PrivateKeySpec.marshal(spec);
|
||||
@@ -74,16 +78,17 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* Ed25519PrivateKeySpec restored = Ed25519PrivateKeySpec.unmarshal(seq);
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Thread-safety</h2> Instances are immutable. The internal key bytes are
|
||||
* defensively copied on construction and retrieval, making this class safe to
|
||||
* share across threads.
|
||||
* <h2>Thread-safety</h2> Access and destruction are synchronized. The internal
|
||||
* key bytes are defensively copied on construction and retrieval.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec {
|
||||
public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec, Destroyable {
|
||||
|
||||
private static final String PKCS8_B64 = "pkcs8.b64";
|
||||
private final byte[] encodedPkcs8;
|
||||
private final ReentrantLock lifecycleLock = new ReentrantLock();
|
||||
private boolean destroyed;
|
||||
|
||||
/**
|
||||
* Creates a new Ed25519 private key specification from its PKCS#8 encoding.
|
||||
@@ -104,7 +109,13 @@ public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return clone of the PKCS#8 encoding
|
||||
*/
|
||||
public byte[] encoded() {
|
||||
return encodedPkcs8.clone();
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return encodedPkcs8.clone();
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,7 +130,7 @@ public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return serialized representation as a {@link PairSeq}
|
||||
*/
|
||||
public static PairSeq marshal(Ed25519PrivateKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8);
|
||||
String b64 = spec.encodedKey();
|
||||
return PairSeq.of("type", "Ed25519-PRIV", PKCS8_B64, b64);
|
||||
}
|
||||
|
||||
@@ -143,12 +154,62 @@ public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec {
|
||||
String k = cur.key();
|
||||
String v = cur.value();
|
||||
if (PKCS8_B64.equals(k)) {
|
||||
out = Base64.getDecoder().decode(v);
|
||||
out = decodeReplacing(out, v);
|
||||
}
|
||||
}
|
||||
if (out == null) {
|
||||
throw new IllegalArgumentException("pkcs8.b64 missing for Ed25519 private key");
|
||||
}
|
||||
return new Ed25519PrivateKeySpec(out);
|
||||
try {
|
||||
return new Ed25519PrivateKeySpec(out);
|
||||
} finally {
|
||||
Arrays.fill(out, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] decodeReplacing(byte[] current, String encoded) {
|
||||
if (current != null) {
|
||||
Arrays.fill(current, (byte) 0);
|
||||
}
|
||||
return Base64.getDecoder().decode(encoded);
|
||||
}
|
||||
|
||||
private String encodedKey() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return Base64.getEncoder().withoutPadding().encodeToString(encodedPkcs8);
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
if (!destroyed) {
|
||||
Arrays.fill(encodedPkcs8, (byte) 0);
|
||||
destroyed = true;
|
||||
}
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
return destroyed;
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureActive() {
|
||||
if (destroyed) {
|
||||
throw new IllegalStateException("Ed25519 private key specification has been destroyed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder;
|
||||
/**
|
||||
* <h2>Public key builder for Ed25519</h2>
|
||||
*
|
||||
* Concrete {@link zeroecho.core.spi.AsymmetricKeyBuilder} for importing and
|
||||
* Concrete {@link zeroecho.core.spi.PublicKeyImporter} for importing
|
||||
* wrapping Ed25519 public keys.
|
||||
*
|
||||
* <p>
|
||||
@@ -59,7 +59,7 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder;
|
||||
* <h2>Usage example</h2> <pre>{@code
|
||||
* // Import a public key from its encoded X.509 form
|
||||
* Ed25519PublicKeySpec spec = new Ed25519PublicKeySpec(x509Bytes);
|
||||
* PublicKey publicKey = CryptoAlgorithms.publicKey("Ed25519", spec);
|
||||
* PublicKey publicKey = session.keyBuilders().asymmetric().importPublic("Ed25519", spec);
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Thread-safety</h2> Instances of this builder are stateless and may be
|
||||
|
||||
@@ -65,7 +65,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* Ed25519PublicKeySpec spec = new Ed25519PublicKeySpec(x509Bytes);
|
||||
*
|
||||
* // Import into a PublicKey using ZeroEcho
|
||||
* PublicKey pub = CryptoAlgorithms.publicKey("Ed25519", spec);
|
||||
* PublicKey pub = session.keyBuilders().asymmetric().importPublic("Ed25519", spec);
|
||||
*
|
||||
* // Serialize to PairSeq (e.g., for configuration or transport)
|
||||
* PairSeq seq = Ed25519PublicKeySpec.marshal(spec);
|
||||
|
||||
@@ -49,8 +49,8 @@
|
||||
* enforces the 64-byte tag size.</li>
|
||||
* <li>Expose builders for key-pair generation and for importing encoded
|
||||
* public/private keys.</li>
|
||||
* <li>Define immutable key specifications suitable for safe cloning and simple
|
||||
* marshalling.</li>
|
||||
* <li>Define defensively copying key specifications suitable for safe cloning
|
||||
* and simple marshalling; private-key specifications are destroyable.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Components</h2>
|
||||
@@ -63,9 +63,9 @@
|
||||
* marker spec for producing key pairs.</li>
|
||||
* <li><b>Ed25519PublicKeyBuilder</b> / <b>Ed25519PrivateKeyBuilder</b>:
|
||||
* importers backed by JCA key factories.</li>
|
||||
* <li><b>Ed25519PublicKeySpec</b> / <b>Ed25519PrivateKeySpec</b>: immutable
|
||||
* wrappers over X.509 and PKCS#8 encodings, with defensive copying and simple
|
||||
* base64 marshalling helpers.</li>
|
||||
* <li><b>Ed25519PublicKeySpec</b> / <b>Ed25519PrivateKeySpec</b>: wrappers over
|
||||
* X.509 and PKCS#8 encodings, with defensive copying and simple base64
|
||||
* marshalling helpers; the private-key form is destroyable.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Design notes</h2>
|
||||
|
||||
@@ -82,12 +82,13 @@ import zeroecho.core.spec.VoidSpec;
|
||||
* <pre>{@code
|
||||
* // Example: generate a key pair and sign data
|
||||
* CryptoAlgorithm ed448 = new Ed448Algorithm();
|
||||
* KeyPair kp = ed448.generateKeyPair(Ed448KeyGenSpec.defaultSpec());
|
||||
* KeyPair kp = ed448.asymmetricKeyPairGenerator(Ed448KeyGenSpec.class)
|
||||
* .generateKeyPair(Ed448KeyGenSpec.defaultSpec());
|
||||
*
|
||||
* SignatureContext signer = ed448.create(KeyUsage.SIGN, kp.getPrivate(), null);
|
||||
* SignatureContext signer = ed448.createContext(KeyUsage.SIGN, kp.getPrivate(), null);
|
||||
* byte[] sig = signer.sign(data);
|
||||
*
|
||||
* SignatureContext verifier = ed448.create(KeyUsage.VERIFY, kp.getPublic(), null);
|
||||
* SignatureContext verifier = ed448.createContext(KeyUsage.VERIFY, kp.getPublic(), null);
|
||||
* boolean ok = verifier.verify(data, sig);
|
||||
* }</pre>
|
||||
*
|
||||
@@ -128,7 +129,7 @@ public final class Ed448Algorithm extends AbstractCryptoAlgorithm {
|
||||
* <pre>{@code
|
||||
* // Example: instantiate and obtain a signer
|
||||
* CryptoAlgorithm ed448 = new Ed448Algorithm();
|
||||
* SignatureContext signer = ed448.create(KeyUsage.SIGN, privateKey, null);
|
||||
* SignatureContext signer = ed448.createContext(KeyUsage.SIGN, privateKey, null);
|
||||
* }</pre>
|
||||
*/
|
||||
public Ed448Algorithm() {
|
||||
@@ -155,8 +156,9 @@ public final class Ed448Algorithm extends AbstractCryptoAlgorithm {
|
||||
}, () -> VoidSpec.INSTANCE);
|
||||
|
||||
// Key builders
|
||||
registerAsymmetricKeyBuilder(Ed448KeyGenSpec.class, new Ed448KeyGenBuilder(), Ed448KeyGenSpec::defaultSpec);
|
||||
registerAsymmetricKeyBuilder(Ed448PublicKeySpec.class, new Ed448PublicKeyBuilder(), null);
|
||||
registerAsymmetricKeyBuilder(Ed448PrivateKeySpec.class, new Ed448PrivateKeyBuilder(), null);
|
||||
registerAsymmetricKeyPairGenerator(Ed448KeyGenSpec.class, new Ed448KeyGenBuilder(),
|
||||
Ed448KeyGenSpec::defaultSpec);
|
||||
registerPublicKeyImporter(Ed448PublicKeySpec.class, new Ed448PublicKeyBuilder());
|
||||
registerPrivateKeyImporter(Ed448PrivateKeySpec.class, new Ed448PrivateKeyBuilder());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ package zeroecho.core.alg.ed448;
|
||||
|
||||
import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
|
||||
/**
|
||||
* <h2>Ed448 Private Key Builder</h2>
|
||||
@@ -66,8 +65,8 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Thread-safety</h2> Stateless and safe for concurrent use. Each call to
|
||||
* {@link AsymmetricKeyBuilder#importPrivate(AlgorithmKeySpec)
|
||||
* importPrivate(Ed448PrivateKeySpec)} creates a new
|
||||
* {@link zeroecho.core.spi.PrivateKeyImporter#importPrivate(AlgorithmKeySpec)}
|
||||
* creates a new
|
||||
* {@link java.security.KeyFactory}.
|
||||
*
|
||||
* @since 1.0
|
||||
|
||||
@@ -33,7 +33,11 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.ed448;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
@@ -41,7 +45,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
/**
|
||||
* <h2>Ed448 Private Key Specification</h2>
|
||||
*
|
||||
* Immutable specification for an Ed448 private key in PKCS#8 encoding.
|
||||
* Destroyable specification for an Ed448 private key in PKCS#8 encoding.
|
||||
*
|
||||
* <p>
|
||||
* This class acts as a typed carrier for encoded private key material,
|
||||
@@ -74,15 +78,16 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* Ed448PrivateKeySpec restored = Ed448PrivateKeySpec.unmarshal(p);
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Thread-safety</h2> Instances are immutable and safe to share across
|
||||
* threads.
|
||||
* <h2>Thread-safety</h2> Access and destruction are synchronized.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class Ed448PrivateKeySpec implements AlgorithmKeySpec {
|
||||
public final class Ed448PrivateKeySpec implements AlgorithmKeySpec, Destroyable {
|
||||
|
||||
private static final String PKCS8_B64 = "pkcs8.b64";
|
||||
private final byte[] encodedPkcs8;
|
||||
private final ReentrantLock lifecycleLock = new ReentrantLock();
|
||||
private boolean destroyed;
|
||||
|
||||
/**
|
||||
* Constructs a new Ed448 private key spec from the given PKCS#8-encoded bytes.
|
||||
@@ -103,7 +108,13 @@ public final class Ed448PrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return cloned PKCS#8-encoded key bytes
|
||||
*/
|
||||
public byte[] encoded() {
|
||||
return encodedPkcs8.clone();
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return encodedPkcs8.clone();
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,7 +130,7 @@ public final class Ed448PrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return a {@link PairSeq} containing the type and base64 data
|
||||
*/
|
||||
public static PairSeq marshal(Ed448PrivateKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8);
|
||||
String b64 = spec.encodedKey();
|
||||
return PairSeq.of("type", "Ed448-PRIV", PKCS8_B64, b64);
|
||||
}
|
||||
|
||||
@@ -142,12 +153,62 @@ public final class Ed448PrivateKeySpec implements AlgorithmKeySpec {
|
||||
String k = cur.key();
|
||||
String v = cur.value();
|
||||
if (PKCS8_B64.equals(k)) {
|
||||
out = Base64.getDecoder().decode(v);
|
||||
out = decodeReplacing(out, v);
|
||||
}
|
||||
}
|
||||
if (out == null) {
|
||||
throw new IllegalArgumentException("pkcs8.b64 missing for Ed448 private key");
|
||||
}
|
||||
return new Ed448PrivateKeySpec(out);
|
||||
try {
|
||||
return new Ed448PrivateKeySpec(out);
|
||||
} finally {
|
||||
Arrays.fill(out, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] decodeReplacing(byte[] current, String encoded) {
|
||||
if (current != null) {
|
||||
Arrays.fill(current, (byte) 0);
|
||||
}
|
||||
return Base64.getDecoder().decode(encoded);
|
||||
}
|
||||
|
||||
private String encodedKey() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return Base64.getEncoder().withoutPadding().encodeToString(encodedPkcs8);
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
if (!destroyed) {
|
||||
Arrays.fill(encodedPkcs8, (byte) 0);
|
||||
destroyed = true;
|
||||
}
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
return destroyed;
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureActive() {
|
||||
if (destroyed) {
|
||||
throw new IllegalStateException("Ed448 private key specification has been destroyed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ package zeroecho.core.alg.ed448;
|
||||
|
||||
import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
|
||||
/**
|
||||
* <h2>Ed448 Public Key Builder</h2>
|
||||
@@ -65,8 +64,8 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Thread-safety</h2> Stateless and safe for concurrent use. Each call to
|
||||
* {@link AsymmetricKeyBuilder#importPublic(AlgorithmKeySpec)
|
||||
* importPublic(Ed448PublicKeySpec)} creates a new
|
||||
* {@link zeroecho.core.spi.PublicKeyImporter#importPublic(AlgorithmKeySpec)}
|
||||
* creates a new
|
||||
* {@link java.security.KeyFactory}.
|
||||
*
|
||||
* @since 1.0
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
* This package wires the Ed448 Edwards-curve Digital Signature Algorithm into
|
||||
* the core layer. It provides the algorithm descriptor, a streaming signature
|
||||
* context with a fixed 114-byte tag length, builders for generating and
|
||||
* importing keys, and immutable key specifications with marshalling helpers.
|
||||
* importing keys, and defensively copying key specifications with marshalling
|
||||
* helpers.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Scope and responsibilities</h2>
|
||||
@@ -49,8 +50,8 @@
|
||||
* enforces the 114-byte tag size.</li>
|
||||
* <li>Expose builders for key-pair generation and for importing encoded
|
||||
* keys.</li>
|
||||
* <li>Define immutable key specifications suitable for safe cloning and simple
|
||||
* marshalling.</li>
|
||||
* <li>Define defensively copying key specifications suitable for safe cloning
|
||||
* and simple marshalling; private-key specifications are destroyable.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Components</h2>
|
||||
@@ -63,9 +64,9 @@
|
||||
* marker spec for producing key pairs.</li>
|
||||
* <li><b>Ed448PublicKeyBuilder</b> / <b>Ed448PrivateKeyBuilder</b>: importers
|
||||
* backed by JCA key factories.</li>
|
||||
* <li><b>Ed448PublicKeySpec</b> / <b>Ed448PrivateKeySpec</b>: immutable
|
||||
* wrappers over X.509 and PKCS#8 encodings, with defensive copying and base64
|
||||
* marshalling helpers.</li>
|
||||
* <li><b>Ed448PublicKeySpec</b> / <b>Ed448PrivateKeySpec</b>: wrappers over
|
||||
* X.509 and PKCS#8 encodings, with defensive copying and base64 marshalling
|
||||
* helpers; the private-key form is destroyable.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Design notes</h2>
|
||||
|
||||
@@ -47,6 +47,7 @@ import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.bouncycastle.jce.spec.ElGamalParameterSpec;
|
||||
|
||||
@@ -54,7 +55,9 @@ import zeroecho.core.AlgorithmFamily;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||
import zeroecho.core.context.EncryptionContext;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.AsymmetricKeyPairGenerator;
|
||||
import zeroecho.core.spi.PrivateKeyImporter;
|
||||
import zeroecho.core.spi.PublicKeyImporter;
|
||||
|
||||
/**
|
||||
* <h2>ElGamal Asymmetric Encryption Algorithm</h2>
|
||||
@@ -110,11 +113,12 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
*
|
||||
* <h2>Example</h2> <pre>{@code
|
||||
* CryptoAlgorithm algo = new ElgamalAlgorithm();
|
||||
* KeyPair kp = algo.generateKeyPair(ElgamalParamSpec.ffdhe2048());
|
||||
* KeyPair kp = algo.asymmetricKeyPairGenerator(ElgamalParamSpec.class)
|
||||
* .generateKeyPair(ElgamalParamSpec.ffdhe2048());
|
||||
*
|
||||
* EncryptionContext enc = algo.create(KeyUsage.ENCRYPT, kp.getPublic(),
|
||||
* EncryptionContext enc = algo.createContext(KeyUsage.ENCRYPT, kp.getPublic(),
|
||||
* ElgamalEncSpec.pkcs1());
|
||||
* EncryptionContext dec = algo.create(KeyUsage.DECRYPT, kp.getPrivate(),
|
||||
* EncryptionContext dec = algo.createContext(KeyUsage.DECRYPT, kp.getPrivate(),
|
||||
* ElgamalEncSpec.pkcs1());
|
||||
* }</pre>
|
||||
*
|
||||
@@ -148,7 +152,7 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm {
|
||||
|
||||
if (false) { // NOPMD
|
||||
// this key generation is slow
|
||||
registerAsymmetricKeyBuilder(ElgamalKeyGenSpec.class, new AsymmetricKeyBuilder<>() {
|
||||
registerAsymmetricKeyPairGenerator(ElgamalKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(ElgamalKeyGenSpec spec) throws GeneralSecurityException {
|
||||
ensureBC();
|
||||
@@ -161,20 +165,10 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm {
|
||||
kpg.initialize(eg, new SecureRandom());
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(ElgamalKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException("Use ElgamalPublicKeySpec to import a public key.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(ElgamalKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException("Use ElgamalPrivateKeySpec to import a private key.");
|
||||
}
|
||||
}, ElgamalKeyGenSpec::elgamal2048);
|
||||
}
|
||||
|
||||
registerAsymmetricKeyBuilder(ElgamalParamSpec.class, new AsymmetricKeyBuilder<>() {
|
||||
registerAsymmetricKeyPairGenerator(ElgamalParamSpec.class, new AsymmetricKeyPairGenerator<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(ElgamalParamSpec spec) throws GeneralSecurityException {
|
||||
ensureBC();
|
||||
@@ -183,23 +177,9 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm {
|
||||
kpg.initialize(eg, new SecureRandom());
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(ElgamalParamSpec spec) {
|
||||
throw new UnsupportedOperationException("Use ElgamalPublicKeySpec to import a public key.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(ElgamalParamSpec spec) {
|
||||
throw new UnsupportedOperationException("Use ElgamalPrivateKeySpec to import a private key.");
|
||||
}
|
||||
}, ElgamalParamSpec::ffdhe2048);
|
||||
|
||||
registerAsymmetricKeyBuilder(ElgamalPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(ElgamalPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Generation not supported for encoded spec.");
|
||||
}
|
||||
registerPublicKeyImporter(ElgamalPublicKeySpec.class, new PublicKeyImporter<>() {
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(ElgamalPublicKeySpec spec) throws GeneralSecurityException {
|
||||
@@ -207,31 +187,22 @@ public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm {
|
||||
KeyFactory kf = KeyFactory.getInstance(EL_GAMAL, providerName());
|
||||
return kf.generatePublic(new X509EncodedKeySpec(spec.encoded()));
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(ElgamalPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use ElgamalPrivateKeySpec for private keys.");
|
||||
}
|
||||
}, null);
|
||||
|
||||
registerAsymmetricKeyBuilder(ElgamalPrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(ElgamalPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Generation not supported for encoded spec.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(ElgamalPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use ElgamalPublicKeySpec for public keys.");
|
||||
}
|
||||
registerPrivateKeyImporter(ElgamalPrivateKeySpec.class, new PrivateKeyImporter<>() {
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(ElgamalPrivateKeySpec spec) throws GeneralSecurityException {
|
||||
ensureBC();
|
||||
KeyFactory kf = KeyFactory.getInstance(EL_GAMAL, providerName());
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded()));
|
||||
byte[] encoded = spec.encoded();
|
||||
try {
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
|
||||
} finally {
|
||||
Arrays.fill(encoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -137,7 +137,7 @@ public final class ElgamalCipherContext implements EncryptionContext {
|
||||
|
||||
return CipherTransformInputStreamBuilder.builder().withCipher(cipher).withUpstream(upstream)
|
||||
.withInputBlockSize(g.inputBlockSize()).withOutputBlockSize(g.perBlockOutput())
|
||||
.withLeftZeroPadding(g.noPadding).build();
|
||||
.withLeftZeroPadding(g.noPadding).withIndependentBlocks().build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -71,7 +71,7 @@ import zeroecho.core.spec.ContextSpec;
|
||||
*
|
||||
* <h2>Usage</h2> Instances are created via the static factories: <pre>{@code
|
||||
* ElgamalEncSpec spec = ElgamalEncSpec.pkcs1();
|
||||
* EncryptionContext ctx = algo.create(KeyUsage.ENCRYPT, pubKey, spec);
|
||||
* EncryptionContext ctx = algo.createContext(KeyUsage.ENCRYPT, pubKey, spec);
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Thread-safety</h2> {@code ElgamalEncSpec} is immutable and safe to share
|
||||
|
||||
@@ -64,7 +64,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* <h2>Usage</h2> <pre>{@code
|
||||
* ElgamalKeyGenSpec spec = ElgamalKeyGenSpec.elgamal2048();
|
||||
* KeyPair kp = algo.generateKeyPair(spec);
|
||||
* KeyPair kp = algo.asymmetricKeyPairGenerator(ElgamalKeyGenSpec.class).generateKeyPair(spec);
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Thread-safety</h2> Instances are immutable and can be freely shared
|
||||
|
||||
@@ -73,7 +73,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* <h2>Usage</h2> <pre>{@code
|
||||
* ElgamalParamSpec spec = ElgamalParamSpec.ffdhe2048();
|
||||
* KeyPair kp = algo.generateKeyPair(spec);
|
||||
* KeyPair kp = algo.asymmetricKeyPairGenerator(ElgamalParamSpec.class).generateKeyPair(spec);
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Thread-safety</h2> Instances are immutable and safe to share between
|
||||
|
||||
@@ -33,7 +33,11 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.elgamal;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
@@ -62,7 +66,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* // Import from PKCS#8 DER
|
||||
* byte[] der = Files.readAllBytes(Path.of("elgamal-priv.der"));
|
||||
* ElgamalPrivateKeySpec spec = new ElgamalPrivateKeySpec(der);
|
||||
* PrivateKey priv = algo.importPrivate(spec);
|
||||
* PrivateKey priv = algo.privateKeyImporter(ElgamalPrivateKeySpec.class).importPrivate(spec);
|
||||
*
|
||||
* // Marshal for serialization
|
||||
* PairSeq ps = ElgamalPrivateKeySpec.marshal(spec);
|
||||
@@ -79,15 +83,16 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* handling when possible.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Thread-safety</h2> Instances are immutable and safe to share between
|
||||
* threads.
|
||||
* <h2>Thread-safety</h2> Access and destruction are synchronized.
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec {
|
||||
public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
|
||||
|
||||
private static final String PKCS8_B64 = "pkcs8.b64";
|
||||
private final byte[] pkcs8;
|
||||
private final ReentrantLock lifecycleLock = new ReentrantLock();
|
||||
private boolean destroyed;
|
||||
|
||||
/**
|
||||
* Constructs a new private key spec from PKCS#8-encoded bytes.
|
||||
@@ -104,7 +109,13 @@ public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return PKCS#8 DER encoding
|
||||
*/
|
||||
public byte[] encoded() {
|
||||
return pkcs8.clone();
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return pkcs8.clone();
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,7 +133,7 @@ public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return marshalled key as {@link PairSeq}
|
||||
*/
|
||||
public static PairSeq marshal(ElgamalPrivateKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
|
||||
String b64 = spec.encodedKey();
|
||||
return PairSeq.of("type", "ELGAMAL-PRIV", PKCS8_B64, b64);
|
||||
}
|
||||
|
||||
@@ -141,12 +152,62 @@ public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec {
|
||||
String k = cur.key();
|
||||
String v = cur.value();
|
||||
if (PKCS8_B64.equals(k)) {
|
||||
out = Base64.getDecoder().decode(v);
|
||||
out = decodeReplacing(out, v);
|
||||
}
|
||||
}
|
||||
if (out == null) {
|
||||
throw new IllegalArgumentException("pkcs8.b64 missing for ElGamal private key");
|
||||
}
|
||||
return new ElgamalPrivateKeySpec(out);
|
||||
try {
|
||||
return new ElgamalPrivateKeySpec(out);
|
||||
} finally {
|
||||
Arrays.fill(out, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] decodeReplacing(byte[] current, String encoded) {
|
||||
if (current != null) {
|
||||
Arrays.fill(current, (byte) 0);
|
||||
}
|
||||
return Base64.getDecoder().decode(encoded);
|
||||
}
|
||||
|
||||
private String encodedKey() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
if (!destroyed) {
|
||||
Arrays.fill(pkcs8, (byte) 0);
|
||||
destroyed = true;
|
||||
}
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
return destroyed;
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureActive() {
|
||||
if (destroyed) {
|
||||
throw new IllegalStateException("ElGamal private key specification has been destroyed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* // Import from X.509 DER
|
||||
* byte[] der = Files.readAllBytes(Path.of("elgamal-pub.der"));
|
||||
* ElgamalPublicKeySpec spec = new ElgamalPublicKeySpec(der);
|
||||
* PublicKey pub = algo.importPublic(spec);
|
||||
* PublicKey pub = algo.publicKeyImporter(ElgamalPublicKeySpec.class).importPublic(spec);
|
||||
*
|
||||
* // Marshal for serialization
|
||||
* PairSeq ps = ElgamalPublicKeySpec.marshal(spec);
|
||||
|
||||
@@ -70,9 +70,9 @@
|
||||
* <li><b>ElgamalKeyGenSpec</b>: parameters for generating fresh domain
|
||||
* parameters and key pairs; typically disabled in favor of predefined parameter
|
||||
* sets.</li>
|
||||
* <li><b>ElgamalPublicKeySpec</b> / <b>ElgamalPrivateKeySpec</b>: immutable
|
||||
* encoded key specifications (X.509 and PKCS#8) with defensive copying and
|
||||
* compact marshalling helpers.</li>
|
||||
* <li><b>ElgamalPublicKeySpec</b> / <b>ElgamalPrivateKeySpec</b>: encoded key
|
||||
* specifications (X.509 and PKCS#8) with defensive copying and compact
|
||||
* marshalling helpers; the private-key form is destroyable.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Design notes</h2>
|
||||
|
||||
@@ -44,6 +44,7 @@ import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
|
||||
@@ -56,7 +57,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter;
|
||||
import zeroecho.core.context.KemContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.core.spec.VoidSpec;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.AsymmetricKeyPairGenerator;
|
||||
import zeroecho.core.spi.PrivateKeyImporter;
|
||||
import zeroecho.core.spi.PublicKeyImporter;
|
||||
|
||||
/**
|
||||
* <h2>Frodo Key Encapsulation Mechanism (KEM)</h2>
|
||||
@@ -104,9 +107,8 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
* X.509 encoding.</li>
|
||||
* <li>Private keys may be imported from {@link FrodoPrivateKeySpec} using a
|
||||
* PKCS#8 encoding.</li>
|
||||
* <li>Direct import of key specs via {@code generateKeyPair} in the spec-based
|
||||
* builders is not supported and will throw
|
||||
* {@link UnsupportedOperationException}.</li>
|
||||
* <li>Generation and public/private import are registered as independent exact
|
||||
* capabilities.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Provider requirements</h2>
|
||||
@@ -120,13 +122,14 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
* <h2>Example usage</h2> <pre>{@code
|
||||
* // Generate a Frodo keypair
|
||||
* CryptoAlgorithm frodo = CryptoAlgorithms.require("Frodo");
|
||||
* KeyPair kp = frodo.generateKeyPair(FrodoKeyGenSpec.frodo1344aes());
|
||||
* KeyPair kp = frodo.asymmetricKeyPairGenerator(FrodoKeyGenSpec.class)
|
||||
* .generateKeyPair(FrodoKeyGenSpec.frodo1344aes());
|
||||
*
|
||||
* // Encapsulate using the recipient's public key
|
||||
* KemContext enc = frodo.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
|
||||
* KemContext enc = frodo.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
|
||||
*
|
||||
* // Decapsulate using the recipient's private key
|
||||
* KemContext dec = frodo.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
|
||||
* KemContext dec = frodo.createContext(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Thread-safety</h2>
|
||||
@@ -201,7 +204,7 @@ public final class FrodoAlgorithm extends AbstractCryptoAlgorithm {
|
||||
.build();
|
||||
}, () -> VoidSpec.INSTANCE);
|
||||
|
||||
registerAsymmetricKeyBuilder(FrodoKeyGenSpec.class, new AsymmetricKeyBuilder<>() {
|
||||
registerAsymmetricKeyPairGenerator(FrodoKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(FrodoKeyGenSpec spec) throws GeneralSecurityException {
|
||||
ensureProvider();
|
||||
@@ -217,23 +220,9 @@ public final class FrodoAlgorithm extends AbstractCryptoAlgorithm {
|
||||
kpg.initialize(params, new SecureRandom());
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(FrodoKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(FrodoKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}, FrodoKeyGenSpec::frodo1344aes);
|
||||
|
||||
registerAsymmetricKeyBuilder(FrodoPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(FrodoPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
registerPublicKeyImporter(FrodoPublicKeySpec.class, new PublicKeyImporter<>() {
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(FrodoPublicKeySpec spec) throws GeneralSecurityException {
|
||||
@@ -241,31 +230,22 @@ public final class FrodoAlgorithm extends AbstractCryptoAlgorithm {
|
||||
KeyFactory kf = KeyFactory.getInstance("Frodo", providerName());
|
||||
return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(FrodoPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}, null);
|
||||
|
||||
registerAsymmetricKeyBuilder(FrodoPrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(FrodoPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(FrodoPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
registerPrivateKeyImporter(FrodoPrivateKeySpec.class, new PrivateKeyImporter<>() {
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(FrodoPrivateKeySpec spec) throws GeneralSecurityException {
|
||||
ensureProvider();
|
||||
KeyFactory kf = KeyFactory.getInstance("Frodo", providerName());
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8()));
|
||||
byte[] encoded = spec.pkcs8();
|
||||
try {
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
|
||||
} finally {
|
||||
Arrays.fill(encoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
});
|
||||
}
|
||||
|
||||
private static void ensureProvider() throws NoSuchProviderException {
|
||||
|
||||
@@ -33,10 +33,13 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.frodo;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.marshal.PairSeq.Cursor;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
@@ -45,7 +48,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* <h2>Specification for importing a Frodo private key</h2>
|
||||
*
|
||||
* {@code FrodoPrivateKeySpec} is a simple wrapper around a PKCS#8-encoded
|
||||
* FrodoKEM private key. It provides immutable access to the raw encoding and
|
||||
* FrodoKEM private key. It provides defensive access to the raw encoding and
|
||||
* utilities for serialization.
|
||||
*
|
||||
* <h2>Encoding format</h2>
|
||||
@@ -59,9 +62,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* <h2>Usage</h2>
|
||||
* <ul>
|
||||
* <li>Instances of this spec can be passed to
|
||||
* {@link CryptoAlgorithm#importPrivate(AlgorithmKeySpec)
|
||||
* importPrivate(FrodoPrivateKeySpec)} to construct a usable
|
||||
* {@link java.security.PrivateKey} object.</li>
|
||||
* {@link zeroecho.sdk.KeyBuilders.Asymmetric#importPrivate(String, AlgorithmKeySpec)}
|
||||
* to construct a usable {@link java.security.PrivateKey} object.</li>
|
||||
* <li>The {@link #marshal(FrodoPrivateKeySpec)} and {@link #unmarshal(PairSeq)}
|
||||
* helpers allow safe conversion to/from structured textual form for persistence
|
||||
* or transmission.</li>
|
||||
@@ -71,21 +73,23 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* // Import an existing Frodo private key
|
||||
* byte[] encoded = Files.readAllBytes(Paths.get("frodo.key"));
|
||||
* FrodoPrivateKeySpec spec = new FrodoPrivateKeySpec(encoded);
|
||||
* PrivateKey priv = frodo.importPrivate(spec);
|
||||
* PrivateKey priv = session.keyBuilders().asymmetric().importPrivate("FrodoKEM", spec);
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Thread-safety</h2>
|
||||
* <p>
|
||||
* This class is immutable; the internal byte array is cloned on construction
|
||||
* and when accessed via {@link #pkcs8()}.
|
||||
* The internal byte array is cloned on construction and when accessed via
|
||||
* {@link #pkcs8()}. Access and destruction are synchronized.
|
||||
* </p>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class FrodoPrivateKeySpec implements AlgorithmKeySpec {
|
||||
public final class FrodoPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
|
||||
|
||||
private static final String PKCS8_B64 = "pkcs8.b64";
|
||||
private final byte[] pkcs8;
|
||||
private final ReentrantLock lifecycleLock = new ReentrantLock();
|
||||
private boolean destroyed;
|
||||
|
||||
/**
|
||||
* Creates a new specification from a PKCS#8 DER-encoded key.
|
||||
@@ -103,7 +107,13 @@ public final class FrodoPrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return defensive copy of the PKCS#8-encoded private key
|
||||
*/
|
||||
public byte[] pkcs8() {
|
||||
return pkcs8.clone();
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return pkcs8.clone();
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,7 +124,7 @@ public final class FrodoPrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return a {@code PairSeq} with type and Base64-encoded key
|
||||
*/
|
||||
public static PairSeq marshal(FrodoPrivateKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
|
||||
String b64 = spec.encodedKey();
|
||||
return PairSeq.of("type", "FrodoPrivateKeySpec", PKCS8_B64, b64);
|
||||
}
|
||||
|
||||
@@ -136,7 +146,12 @@ public final class FrodoPrivateKeySpec implements AlgorithmKeySpec {
|
||||
if (b64 == null) {
|
||||
throw new IllegalArgumentException("FrodoPrivateKeySpec: missing pkcs8.b64");
|
||||
}
|
||||
return new FrodoPrivateKeySpec(Base64.getDecoder().decode(b64));
|
||||
byte[] decoded = Base64.getDecoder().decode(b64);
|
||||
try {
|
||||
return new FrodoPrivateKeySpec(decoded);
|
||||
} finally {
|
||||
Arrays.fill(decoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,4 +163,43 @@ public final class FrodoPrivateKeySpec implements AlgorithmKeySpec {
|
||||
public String toString() {
|
||||
return "FrodoPrivateKeySpec[len=" + pkcs8.length + "]";
|
||||
}
|
||||
|
||||
private String encodedKey() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
if (!destroyed) {
|
||||
Arrays.fill(pkcs8, (byte) 0);
|
||||
destroyed = true;
|
||||
}
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
return destroyed;
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureActive() {
|
||||
if (destroyed) {
|
||||
throw new IllegalStateException("Frodo private key specification has been destroyed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ package zeroecho.core.alg.frodo;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.core.CryptoAlgorithm;
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.marshal.PairSeq.Cursor;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
@@ -59,9 +58,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* <h2>Usage</h2>
|
||||
* <ul>
|
||||
* <li>Instances of this spec can be passed to
|
||||
* {@link CryptoAlgorithm#importPublic(AlgorithmKeySpec)
|
||||
* importPublic(FrodoPublicKeySpec)} to construct a usable
|
||||
* {@link java.security.PublicKey}.</li>
|
||||
* {@link zeroecho.sdk.KeyBuilders.Asymmetric#importPublic(String, AlgorithmKeySpec)}
|
||||
* to construct a usable {@link java.security.PublicKey}.</li>
|
||||
* <li>The {@link #marshal(FrodoPublicKeySpec)} and {@link #unmarshal(PairSeq)}
|
||||
* methods allow safe serialization into and recovery from structured textual
|
||||
* form.</li>
|
||||
@@ -71,7 +69,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* // Import an existing Frodo public key
|
||||
* byte[] encoded = Files.readAllBytes(Paths.get("frodo.pub"));
|
||||
* FrodoPublicKeySpec spec = new FrodoPublicKeySpec(encoded);
|
||||
* PublicKey pub = frodo.importPublic(spec);
|
||||
* PublicKey pub = session.keyBuilders().asymmetric().importPublic("FrodoKEM", spec);
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Thread-safety</h2>
|
||||
|
||||
@@ -51,8 +51,9 @@
|
||||
* role for initiator/responder workflows.</li>
|
||||
* <li>Provide a {@link zeroecho.core.context.KemContext} implementation bound
|
||||
* to either a public or private key for encapsulation or decapsulation.</li>
|
||||
* <li>Expose immutable specifications for key generation variants and encoded
|
||||
* key carriers with marshalling helpers.</li>
|
||||
* <li>Expose immutable key-generation specifications and defensively copying
|
||||
* encoded-key carriers with marshalling helpers; private-key carriers are
|
||||
* destroyable.</li>
|
||||
* <li>Ensure operations are delegated to a supported PQC provider (BouncyCastle
|
||||
* PQC) and fail fast if absent.</li>
|
||||
* </ul>
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.hmac;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
@@ -45,7 +45,9 @@ import zeroecho.core.AlgorithmFamily;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.alg.AbstractCryptoAlgorithm;
|
||||
import zeroecho.core.context.MacContext;
|
||||
import zeroecho.core.spi.SymmetricKeyBuilder;
|
||||
import zeroecho.core.err.ProviderFailureException;
|
||||
import zeroecho.core.spi.SymmetricKeyGenerator;
|
||||
import zeroecho.core.spi.SymmetricKeyImporter;
|
||||
|
||||
/**
|
||||
* <h2>HMAC Algorithm Integration</h2>
|
||||
@@ -86,11 +88,11 @@ import zeroecho.core.spi.SymmetricKeyBuilder;
|
||||
*
|
||||
* <h2>Usage example</h2> <pre>{@code
|
||||
* // Generate a fresh key for HMAC-SHA256
|
||||
* SecretKey key = CryptoAlgorithms.generateSecret("HMAC",
|
||||
* SecretKey key = session.keyBuilders().symmetric().generate("HMAC",
|
||||
* HmacKeyGenSpec.sha256(256));
|
||||
*
|
||||
* // Create a MAC context
|
||||
* MacContext ctx = CryptoAlgorithms.create("HMAC",
|
||||
* MacContext ctx = session.createContext("HMAC",
|
||||
* KeyUsage.MAC, key, HmacSpec.sha256());
|
||||
*
|
||||
* ctx.update(data);
|
||||
@@ -133,37 +135,33 @@ public final class HmacAlgorithm extends AbstractCryptoAlgorithm {
|
||||
try {
|
||||
return new HmacMacContext(this, k, s.macName());
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IOException("Init HMAC failed for " + s.macName(), e);
|
||||
throw new ProviderFailureException("Failed to initialize HMAC " + s.macName(), e);
|
||||
}
|
||||
}, HmacSpec::sha256 // default for catalog/tests
|
||||
);
|
||||
|
||||
// Key builders (generation/import) — both respect macName in the spec.
|
||||
registerSymmetricKeyBuilder(HmacKeyGenSpec.class, new SymmetricKeyBuilder<>() {
|
||||
registerSymmetricKeyGenerator(HmacKeyGenSpec.class, new SymmetricKeyGenerator<>() {
|
||||
@Override
|
||||
public SecretKey generateSecret(HmacKeyGenSpec spec) throws GeneralSecurityException {
|
||||
KeyGenerator kg = KeyGenerator.getInstance(spec.macName());
|
||||
kg.init(spec.keySizeBits(), new SecureRandom());
|
||||
return kg.generateKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SecretKey importSecret(HmacKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException("Use HmacKeyImportSpec for import");
|
||||
}
|
||||
}, () -> HmacKeyGenSpec.sha256(256) // default keygen spec
|
||||
);
|
||||
|
||||
registerSymmetricKeyBuilder(HmacKeyImportSpec.class, new SymmetricKeyBuilder<>() {
|
||||
@Override
|
||||
public SecretKey generateSecret(HmacKeyImportSpec spec) {
|
||||
throw new UnsupportedOperationException("Use HmacKeyGenSpec for generation");
|
||||
}
|
||||
registerSymmetricKeyImporter(HmacKeyImportSpec.class, new SymmetricKeyImporter<>() {
|
||||
|
||||
@Override
|
||||
public SecretKey importSecret(HmacKeyImportSpec spec) {
|
||||
return new SecretKeySpec(spec.key(), spec.macName());
|
||||
byte[] key = spec.key();
|
||||
try {
|
||||
return new SecretKeySpec(key, spec.macName());
|
||||
} finally {
|
||||
Arrays.fill(key, (byte) 0);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,12 +55,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* <h2>Usage</h2> Typical usage is to construct a spec with a given digest
|
||||
* family and key size, and then pass it to a registered
|
||||
* {@code SymmetricKeyBuilder}:
|
||||
* {@link zeroecho.core.spi.SymmetricKeyGenerator}:
|
||||
*
|
||||
* <pre>{@code
|
||||
* // Generate a 256-bit key for HMAC-SHA256
|
||||
* HmacKeyGenSpec spec = HmacKeyGenSpec.sha256(256);
|
||||
* SecretKey key = CryptoAlgorithms.generateSecret("HMAC", spec);
|
||||
* SecretKey key = session.keyBuilders().symmetric().generate("HMAC", spec);
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Defaults</h2> Convenience static factories are provided for the most
|
||||
|
||||
@@ -34,8 +34,12 @@
|
||||
package zeroecho.core.alg.hmac;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.Arrays;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import zeroecho.core.annotation.Describable;
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
@@ -66,7 +70,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* {@code
|
||||
* byte[] rawKey = Files.readAllBytes(Paths.get("hmac.key"));
|
||||
* HmacKeyImportSpec spec = HmacKeyImportSpec.fromRaw("HmacSHA256", rawKey);
|
||||
* SecretKey key = CryptoAlgorithms.importSecret("HMAC", spec);
|
||||
* SecretKey key = session.keyBuilders().symmetric().importKey("HMAC", spec);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
@@ -100,9 +104,11 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable {
|
||||
public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable, Destroyable {
|
||||
private final String macName;
|
||||
private final byte[] key;
|
||||
private final ReentrantLock lifecycleLock = new ReentrantLock();
|
||||
private boolean destroyed;
|
||||
|
||||
/**
|
||||
* Constructs a new HMAC key import specification.
|
||||
@@ -132,7 +138,13 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable {
|
||||
* @return cloned key bytes
|
||||
*/
|
||||
public byte[] key() {
|
||||
return key.clone();
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return key.clone();
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,7 +168,12 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable {
|
||||
*/
|
||||
public static HmacKeyImportSpec fromHex(String macName, String hex) {
|
||||
Objects.requireNonNull(hex, "hex must not be null");
|
||||
return fromRaw(macName, HexFormat.of().parseHex(hex));
|
||||
byte[] decoded = HexFormat.of().parseHex(hex);
|
||||
try {
|
||||
return fromRaw(macName, decoded);
|
||||
} finally {
|
||||
Arrays.fill(decoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,7 +186,12 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable {
|
||||
*/
|
||||
public static HmacKeyImportSpec fromBase64(String macName, String b64) {
|
||||
Objects.requireNonNull(b64, "base64 must not be null");
|
||||
return fromRaw(macName, Base64.getDecoder().decode(b64));
|
||||
byte[] decoded = Base64.getDecoder().decode(b64);
|
||||
try {
|
||||
return fromRaw(macName, decoded);
|
||||
} finally {
|
||||
Arrays.fill(decoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,7 +215,7 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable {
|
||||
* @return encoded key spec sequence
|
||||
*/
|
||||
public static PairSeq marshal(HmacKeyImportSpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.key);
|
||||
String b64 = spec.encodedKey();
|
||||
return PairSeq.of("type", "HMAC-KEY", "mac", spec.macName, "k.b64", b64);
|
||||
}
|
||||
|
||||
@@ -213,24 +235,81 @@ public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable {
|
||||
String mac = null;
|
||||
byte[] key = null;
|
||||
|
||||
PairSeq.Cursor cur = p.cursor();
|
||||
while (cur.next()) {
|
||||
String k = cur.key();
|
||||
String v = cur.value();
|
||||
switch (k) {
|
||||
case "mac" -> mac = v;
|
||||
case "k.b64" -> key = Base64.getDecoder().decode(v);
|
||||
case "k.hex" -> key = HexFormat.of().parseHex(v);
|
||||
default -> {
|
||||
try {
|
||||
PairSeq.Cursor cur = p.cursor();
|
||||
while (cur.next()) {
|
||||
String k = cur.key();
|
||||
String v = cur.value();
|
||||
switch (k) {
|
||||
case "mac" -> mac = v;
|
||||
case "k.b64" -> {
|
||||
wipe(key);
|
||||
key = Base64.getDecoder().decode(v);
|
||||
}
|
||||
case "k.hex" -> {
|
||||
wipe(key);
|
||||
key = HexFormat.of().parseHex(v);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mac == null) {
|
||||
throw new IllegalArgumentException("mac missing for HMAC key");
|
||||
}
|
||||
if (key == null) {
|
||||
throw new IllegalArgumentException("HMAC key missing (k.b64 or k.hex)");
|
||||
}
|
||||
return new HmacKeyImportSpec(mac, key);
|
||||
} finally {
|
||||
wipe(key);
|
||||
}
|
||||
if (mac == null) {
|
||||
throw new IllegalArgumentException("mac missing for HMAC key");
|
||||
}
|
||||
|
||||
private static void wipe(byte[] current) {
|
||||
if (current != null) {
|
||||
Arrays.fill(current, (byte) 0);
|
||||
}
|
||||
if (key == null) {
|
||||
throw new IllegalArgumentException("HMAC key missing (k.b64 or k.hex)");
|
||||
}
|
||||
|
||||
private String encodedKey() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return Base64.getEncoder().withoutPadding().encodeToString(key);
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public void destroy() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
if (!destroyed) {
|
||||
Arrays.fill(key, (byte) 0);
|
||||
destroyed = true;
|
||||
}
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
return destroyed;
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureActive() {
|
||||
if (destroyed) {
|
||||
throw new IllegalStateException("HMAC key import specification has been destroyed");
|
||||
}
|
||||
return new HmacKeyImportSpec(mac, key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate;
|
||||
* <h2>Usage example</h2>
|
||||
* <h3>Produce HMAC-SHA256 trailer</h3> <pre>
|
||||
* {@code
|
||||
* SecretKey key = CryptoAlgorithms.generateSecret("HMAC", HmacKeyGenSpec.sha256(256));
|
||||
* SecretKey key = session.keyBuilders().symmetric().generate("HMAC", HmacKeyGenSpec.sha256(256));
|
||||
* HmacMacContext ctx = new HmacMacContext(CryptoAlgorithms.require("HMAC"), key, "HmacSHA256");
|
||||
* try (InputStream in = ctx.wrap(new FileInputStream("data.bin"))) {
|
||||
* in.transferTo(OutputStream.nullOutputStream()); // body then MAC trailer
|
||||
@@ -135,7 +135,7 @@ public final class HmacMacContext implements MacContext {
|
||||
|
||||
// lifecycle
|
||||
private boolean wrapped; // = false;
|
||||
private Stream activeStream;
|
||||
private HmacStream activeStream;
|
||||
private boolean autoCloseActiveStream;
|
||||
|
||||
/**
|
||||
@@ -238,7 +238,7 @@ public final class HmacMacContext implements MacContext {
|
||||
throw new IOException("HMAC init failed for " + macName, e);
|
||||
}
|
||||
|
||||
Stream s = new Stream(upstream, mac, macName, expectedTag, verifier());
|
||||
HmacStream s = new HmacStream(upstream, mac, macName, expectedTag, verifier());
|
||||
this.activeStream = s;
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -54,10 +54,10 @@ import zeroecho.core.spec.ContextSpec;
|
||||
*
|
||||
* <h2>Usage</h2> This spec is passed when creating a new HMAC context:
|
||||
* <pre>{@code
|
||||
* SecretKey key = CryptoAlgorithms.generateSecret("HMAC",
|
||||
* SecretKey key = session.keyBuilders().symmetric().generate("HMAC",
|
||||
* HmacKeyGenSpec.sha256(256));
|
||||
*
|
||||
* MacContext ctx = CryptoAlgorithms.create("HMAC",
|
||||
* MacContext ctx = session.createContext("HMAC",
|
||||
* KeyUsage.MAC, key, HmacSpec.sha256());
|
||||
*
|
||||
* ctx.update(data);
|
||||
|
||||
@@ -81,7 +81,7 @@ import zeroecho.core.util.Strings;
|
||||
* Mac mac = Mac.getInstance("HmacSHA256");
|
||||
* mac.init(secretKey);
|
||||
* try (InputStream in = Files.newInputStream(path);
|
||||
* InputStream s = new Stream(in, mac, "HmacSHA256", null,
|
||||
* InputStream s = new HmacStream(in, mac, "HmacSHA256", null,
|
||||
* new ByteVerificationStrategy())) {
|
||||
* // read from 's' to consume body; trailer is produced automatically
|
||||
* }
|
||||
@@ -98,14 +98,14 @@ import zeroecho.core.util.Strings;
|
||||
* new ByteVerificationStrategy().getThrowOnMismatch();
|
||||
*
|
||||
* try (InputStream in = Files.newInputStream(path);
|
||||
* InputStream s = new Stream(in, macV, "HmacSHA256", expected, strategy)) {
|
||||
* InputStream s = new HmacStream(in, macV, "HmacSHA256", expected, strategy)) {
|
||||
* // read from 's'; exception is thrown at EOF if verification fails
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
final class Stream extends AbstractPassthroughInputStream {
|
||||
private static final Logger LOG = Logger.getLogger(Stream.class.getName());
|
||||
final class HmacStream extends AbstractPassthroughInputStream {
|
||||
private static final Logger LOG = Logger.getLogger(HmacStream.class.getName());
|
||||
|
||||
private final Mac mac;
|
||||
private final String macName;
|
||||
@@ -135,7 +135,7 @@ final class Stream extends AbstractPassthroughInputStream {
|
||||
* @throws NullPointerException if {@code upstream}, {@code mac}, or
|
||||
* {@code macName} is {@code null}
|
||||
*/
|
||||
/* package */ Stream(final InputStream upstream, final Mac mac, final String macName, final byte[] expectedTag,
|
||||
/* package */ HmacStream(final InputStream upstream, final Mac mac, final String macName, final byte[] expectedTag,
|
||||
final VerificationBiPredicate<byte[]> verificationStrategy) {
|
||||
super(upstream, 8192);
|
||||
this.mac = mac;
|
||||
@@ -48,8 +48,8 @@
|
||||
* <li>Expose a streaming {@link zeroecho.core.context.MacContext} that appends
|
||||
* tags in produce mode or verifies an expected tag at end of stream in verify
|
||||
* mode.</li>
|
||||
* <li>Provide immutable specs for selecting the HMAC variant and for supplying
|
||||
* keys (generation or import of raw key material).</li>
|
||||
* <li>Provide immutable specs for selecting the HMAC variant and destroyable
|
||||
* specs for importing raw key material.</li>
|
||||
* <li>Encapsulate JCA/JCE interop and provider checks behind small
|
||||
* factories.</li>
|
||||
* </ul>
|
||||
@@ -68,7 +68,7 @@
|
||||
* specific HMAC variant.</li>
|
||||
* <li><b>HmacKeyImportSpec</b>: wrapper for importing existing raw keys, with
|
||||
* Base64/hex helpers.</li>
|
||||
* <li><b>Stream</b>: internal passthrough input stream implementing the
|
||||
* <li><b>HmacStream</b>: internal passthrough input stream implementing the
|
||||
* byte-pumping and trailer/verification logic for the MAC context.</li>
|
||||
* </ul>
|
||||
*
|
||||
|
||||
@@ -44,6 +44,7 @@ import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
|
||||
@@ -56,7 +57,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter;
|
||||
import zeroecho.core.context.KemContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.core.spec.VoidSpec;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.AsymmetricKeyPairGenerator;
|
||||
import zeroecho.core.spi.PrivateKeyImporter;
|
||||
import zeroecho.core.spi.PublicKeyImporter;
|
||||
|
||||
/**
|
||||
* <h2>HQC (Hamming Quasi-Cyclic) Algorithm Integration</h2>
|
||||
@@ -124,13 +127,14 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
* <h2>Usage example</h2> <pre>{@code
|
||||
* // Generate an HQC key pair
|
||||
* HqcAlgorithm hqc = new HqcAlgorithm();
|
||||
* KeyPair kp = hqc.generateKeyPair(HqcKeyGenSpec.hqc256());
|
||||
* KeyPair kp = hqc.asymmetricKeyPairGenerator(HqcKeyGenSpec.class)
|
||||
* .generateKeyPair(HqcKeyGenSpec.hqc256());
|
||||
*
|
||||
* // Encapsulation by initiator
|
||||
* KemContext encapsCtx = hqc.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
|
||||
* KemContext encapsCtx = hqc.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
|
||||
*
|
||||
* // Decapsulation by responder
|
||||
* KemContext decapsCtx = hqc.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
|
||||
* KemContext decapsCtx = hqc.createContext(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
|
||||
* }</pre>
|
||||
*
|
||||
* @since 1.0
|
||||
@@ -200,7 +204,7 @@ public final class HqcAlgorithm extends AbstractCryptoAlgorithm {
|
||||
.build();
|
||||
}, () -> VoidSpec.INSTANCE);
|
||||
|
||||
registerAsymmetricKeyBuilder(HqcKeyGenSpec.class, new AsymmetricKeyBuilder<>() {
|
||||
registerAsymmetricKeyPairGenerator(HqcKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(HqcKeyGenSpec spec) throws GeneralSecurityException {
|
||||
ensureProvider();
|
||||
@@ -213,23 +217,9 @@ public final class HqcAlgorithm extends AbstractCryptoAlgorithm {
|
||||
kpg.initialize(params, new SecureRandom());
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(HqcKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(HqcKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}, HqcKeyGenSpec::hqc256);
|
||||
|
||||
registerAsymmetricKeyBuilder(HqcPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(HqcPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
registerPublicKeyImporter(HqcPublicKeySpec.class, new PublicKeyImporter<>() {
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(HqcPublicKeySpec spec) throws GeneralSecurityException {
|
||||
@@ -237,31 +227,22 @@ public final class HqcAlgorithm extends AbstractCryptoAlgorithm {
|
||||
KeyFactory kf = KeyFactory.getInstance("HQC", providerName());
|
||||
return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(HqcPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}, null);
|
||||
|
||||
registerAsymmetricKeyBuilder(HqcPrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
@Override
|
||||
public KeyPair generateKeyPair(HqcPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey importPublic(HqcPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
registerPrivateKeyImporter(HqcPrivateKeySpec.class, new PrivateKeyImporter<>() {
|
||||
|
||||
@Override
|
||||
public PrivateKey importPrivate(HqcPrivateKeySpec spec) throws GeneralSecurityException {
|
||||
ensureProvider();
|
||||
KeyFactory kf = KeyFactory.getInstance("HQC", providerName());
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8()));
|
||||
byte[] encoded = spec.pkcs8();
|
||||
try {
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
|
||||
} finally {
|
||||
Arrays.fill(encoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
});
|
||||
}
|
||||
|
||||
private static void ensureProvider() throws NoSuchProviderException {
|
||||
|
||||
@@ -65,7 +65,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* // Generate a key pair via HqcAlgorithm
|
||||
* HqcAlgorithm hqc = new HqcAlgorithm();
|
||||
* KeyPair kp = hqc.generateKeyPair(spec);
|
||||
* KeyPair kp = hqc.asymmetricKeyPairGenerator(HqcKeyGenSpec.class).generateKeyPair(spec);
|
||||
* }</pre>
|
||||
*
|
||||
* @since 1.0
|
||||
|
||||
@@ -33,8 +33,12 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.hqc;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.marshal.PairSeq.Cursor;
|
||||
@@ -48,8 +52,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* <p>
|
||||
* This class is used to transport and import HQC private keys into the
|
||||
* {@link HqcAlgorithm}. The encoded form is immutable and defensively copied on
|
||||
* construction and retrieval.
|
||||
* {@link HqcAlgorithm}. The encoded form is defensively copied on construction
|
||||
* and retrieval and may be destroyed.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Serialization</h2>
|
||||
@@ -73,7 +77,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* // Import into a PrivateKey via HqcAlgorithm
|
||||
* HqcAlgorithm hqc = new HqcAlgorithm();
|
||||
* PrivateKey priv = hqc.importPrivate(spec);
|
||||
* PrivateKey priv = hqc.privateKeyImporter(HqcPrivateKeySpec.class).importPrivate(spec);
|
||||
*
|
||||
* // Serialize for transport
|
||||
* PairSeq serialized = HqcPrivateKeySpec.marshal(spec);
|
||||
@@ -84,10 +88,12 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class HqcPrivateKeySpec implements AlgorithmKeySpec {
|
||||
public final class HqcPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
|
||||
|
||||
private static final String PKCS8_B64 = "pkcs8.b64";
|
||||
private final byte[] pkcs8;
|
||||
private final ReentrantLock lifecycleLock = new ReentrantLock();
|
||||
private boolean destroyed;
|
||||
|
||||
/**
|
||||
* Constructs a new private key spec from a PKCS#8-encoded byte array.
|
||||
@@ -105,7 +111,13 @@ public final class HqcPrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return cloned PKCS#8 byte array
|
||||
*/
|
||||
public byte[] pkcs8() {
|
||||
return pkcs8.clone();
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return pkcs8.clone();
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,7 +134,7 @@ public final class HqcPrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return serialized representation in a {@link PairSeq}
|
||||
*/
|
||||
public static PairSeq marshal(HqcPrivateKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
|
||||
String b64 = spec.encodedKey();
|
||||
return PairSeq.of("type", "HqcPrivateKeySpec", PKCS8_B64, b64);
|
||||
}
|
||||
|
||||
@@ -144,7 +156,12 @@ public final class HqcPrivateKeySpec implements AlgorithmKeySpec {
|
||||
if (b64 == null) {
|
||||
throw new IllegalArgumentException("HqcPrivateKeySpec: missing pkcs8.b64");
|
||||
}
|
||||
return new HqcPrivateKeySpec(Base64.getDecoder().decode(b64));
|
||||
byte[] decoded = Base64.getDecoder().decode(b64);
|
||||
try {
|
||||
return new HqcPrivateKeySpec(decoded);
|
||||
} finally {
|
||||
Arrays.fill(decoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,4 +177,43 @@ public final class HqcPrivateKeySpec implements AlgorithmKeySpec {
|
||||
public String toString() {
|
||||
return "HqcPrivateKeySpec[len=" + pkcs8.length + "]";
|
||||
}
|
||||
|
||||
private String encodedKey() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
if (!destroyed) {
|
||||
Arrays.fill(pkcs8, (byte) 0);
|
||||
destroyed = true;
|
||||
}
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
return destroyed;
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureActive() {
|
||||
if (destroyed) {
|
||||
throw new IllegalStateException("HQC private key specification has been destroyed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
*
|
||||
* // Import into a PublicKey via HqcAlgorithm
|
||||
* HqcAlgorithm hqc = new HqcAlgorithm();
|
||||
* PublicKey pub = hqc.importPublic(spec);
|
||||
* PublicKey pub = hqc.publicKeyImporter(HqcPublicKeySpec.class).importPublic(spec);
|
||||
*
|
||||
* // Serialize for transport
|
||||
* PairSeq serialized = HqcPublicKeySpec.marshal(spec);
|
||||
|
||||
@@ -49,8 +49,9 @@
|
||||
* workflows.</li>
|
||||
* <li>Provide a {@link zeroecho.core.context.KemContext} bound to a public or
|
||||
* private key for encapsulation or decapsulation.</li>
|
||||
* <li>Expose immutable specifications for key generation variants and encoded
|
||||
* key carriers with simple marshalling helpers.</li>
|
||||
* <li>Expose immutable key-generation specifications and defensively copying
|
||||
* encoded-key carriers with simple marshalling helpers; private-key carriers
|
||||
* are destroyable.</li>
|
||||
* <li>Ensure operations use a supported PQC provider and fail fast if the
|
||||
* provider is absent.</li>
|
||||
* </ul>
|
||||
|
||||
@@ -44,6 +44,7 @@ import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Arrays;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
|
||||
@@ -56,7 +57,9 @@ import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter;
|
||||
import zeroecho.core.context.KemContext;
|
||||
import zeroecho.core.context.MessageAgreementContext;
|
||||
import zeroecho.core.spec.VoidSpec;
|
||||
import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
import zeroecho.core.spi.AsymmetricKeyPairGenerator;
|
||||
import zeroecho.core.spi.PrivateKeyImporter;
|
||||
import zeroecho.core.spi.PublicKeyImporter;
|
||||
|
||||
/**
|
||||
* Concrete CryptoAlgorithm implementation for the post-quantum key
|
||||
@@ -112,22 +115,22 @@ import zeroecho.core.spi.AsymmetricKeyBuilder;
|
||||
* KyberAlgorithm kyber = new KyberAlgorithm();
|
||||
*
|
||||
* // Generate a Kyber-768 key pair:
|
||||
* KeyPair kp = kyber.asymmetricKeyBuilder(KyberKeyGenSpec.class)
|
||||
* KeyPair kp = kyber.asymmetricKeyPairGenerator(KyberKeyGenSpec.class)
|
||||
* .generateKeyPair(KyberKeyGenSpec.kyber768());
|
||||
*
|
||||
* // Encapsulation by initiator (recipient public key known):
|
||||
* KemContext enc = kyber.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
|
||||
* KemContext enc = kyber.createContext(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
|
||||
*
|
||||
* // Decapsulation by responder (own private key):
|
||||
* KemContext dec = kyber.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
|
||||
* KemContext dec = kyber.createContext(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
|
||||
*
|
||||
* // Message-style agreement (initiator):
|
||||
* MessageAgreementContext initCtx =
|
||||
* kyber.create(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE);
|
||||
* kyber.createContext(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE);
|
||||
*
|
||||
* // Message-style agreement (responder):
|
||||
* MessageAgreementContext respCtx =
|
||||
* kyber.create(KeyUsage.AGREEMENT, kp.getPrivate(), VoidSpec.INSTANCE);
|
||||
* kyber.createContext(KeyUsage.AGREEMENT, kp.getPrivate(), VoidSpec.INSTANCE);
|
||||
* }</pre>
|
||||
*/
|
||||
public final class KyberAlgorithm extends AbstractCryptoAlgorithm {
|
||||
@@ -154,7 +157,7 @@ public final class KyberAlgorithm extends AbstractCryptoAlgorithm {
|
||||
* Security.addProvider(new BouncyCastlePQCProvider());
|
||||
* KyberAlgorithm alg = new KyberAlgorithm();
|
||||
*
|
||||
* KeyPair kp = alg.asymmetricKeyBuilder(KyberKeyGenSpec.class)
|
||||
* KeyPair kp = alg.asymmetricKeyPairGenerator(KyberKeyGenSpec.class)
|
||||
* .generateKeyPair(KyberKeyGenSpec.kyber768());
|
||||
* }</pre>
|
||||
*/
|
||||
@@ -189,7 +192,7 @@ public final class KyberAlgorithm extends AbstractCryptoAlgorithm {
|
||||
}, () -> VoidSpec.INSTANCE);
|
||||
|
||||
// Keypair builder via BCPQC
|
||||
registerAsymmetricKeyBuilder(KyberKeyGenSpec.class, new AsymmetricKeyBuilder<>() {
|
||||
registerAsymmetricKeyPairGenerator(KyberKeyGenSpec.class, new AsymmetricKeyPairGenerator<>() {
|
||||
/**
|
||||
* Generates a Kyber key pair for the variant defined by the provided spec.
|
||||
*
|
||||
@@ -206,53 +209,10 @@ public final class KyberAlgorithm extends AbstractCryptoAlgorithm {
|
||||
kpg.initialize(params, new SecureRandom());
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported operation for this builder. Use {@code KyberPublicKeySpec} for
|
||||
* public key import.
|
||||
*
|
||||
* @param spec the key generation spec; not used.
|
||||
* @return never returns normally.
|
||||
* @throws UnsupportedOperationException always thrown to indicate that public
|
||||
* key import uses a dedicated encoded
|
||||
* spec.
|
||||
*/
|
||||
@Override
|
||||
public PublicKey importPublic(KyberKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException("Import with a dedicated encoded spec, if needed.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported operation for this builder. Use {@code KyberPrivateKeySpec} for
|
||||
* private key import.
|
||||
*
|
||||
* @param spec the key generation spec; not used.
|
||||
* @return never returns normally.
|
||||
* @throws UnsupportedOperationException always thrown to indicate that private
|
||||
* key import uses a dedicated encoded
|
||||
* spec.
|
||||
*/
|
||||
@Override
|
||||
public PrivateKey importPrivate(KyberKeyGenSpec spec) {
|
||||
throw new UnsupportedOperationException("Import with a dedicated encoded spec, if needed.");
|
||||
}
|
||||
}, KyberKeyGenSpec::kyber768);
|
||||
|
||||
// Public-key import (X.509)
|
||||
registerAsymmetricKeyBuilder(KyberPublicKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
/**
|
||||
* Unsupported operation for this builder. Use {@code KyberKeyGenSpec} for
|
||||
* generation.
|
||||
*
|
||||
* @param spec the public key spec; not used.
|
||||
* @return never returns normally.
|
||||
* @throws UnsupportedOperationException always thrown to indicate that key
|
||||
* generation is not supported here.
|
||||
*/
|
||||
@Override
|
||||
public KeyPair generateKeyPair(KyberPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use KyberKeyGenSpec for generation");
|
||||
}
|
||||
registerPublicKeyImporter(KyberPublicKeySpec.class, new PublicKeyImporter<>() {
|
||||
|
||||
/**
|
||||
* Imports a Kyber public key from an X.509 SubjectPublicKeyInfo encoding.
|
||||
@@ -269,52 +229,10 @@ public final class KyberAlgorithm extends AbstractCryptoAlgorithm {
|
||||
KeyFactory kf = KeyFactory.getInstance("Kyber", providerName());
|
||||
return kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported operation for this builder. Use {@code KyberPrivateKeySpec} for
|
||||
* private key import.
|
||||
*
|
||||
* @param spec the public key spec; not used.
|
||||
* @return never returns normally.
|
||||
* @throws UnsupportedOperationException always thrown to indicate that private
|
||||
* key import is not supported here.
|
||||
*/
|
||||
@Override
|
||||
public PrivateKey importPrivate(KyberPublicKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use KyberPrivateKeySpec for private key import");
|
||||
}
|
||||
}, null // no default spec
|
||||
);
|
||||
});
|
||||
|
||||
// Private-key import (PKCS#8)
|
||||
registerAsymmetricKeyBuilder(KyberPrivateKeySpec.class, new AsymmetricKeyBuilder<>() {
|
||||
/**
|
||||
* Unsupported operation for this builder. Use {@code KyberKeyGenSpec} for
|
||||
* generation.
|
||||
*
|
||||
* @param spec the private key spec; not used.
|
||||
* @return never returns normally.
|
||||
* @throws UnsupportedOperationException always thrown to indicate that key
|
||||
* generation is not supported here.
|
||||
*/
|
||||
@Override
|
||||
public KeyPair generateKeyPair(KyberPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use KyberKeyGenSpec for generation");
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsupported operation for this builder. Use {@code KyberPublicKeySpec} for
|
||||
* public key import.
|
||||
*
|
||||
* @param spec the private key spec; not used.
|
||||
* @return never returns normally.
|
||||
* @throws UnsupportedOperationException always thrown to indicate that public
|
||||
* key import is not supported here.
|
||||
*/
|
||||
@Override
|
||||
public PublicKey importPublic(KyberPrivateKeySpec spec) {
|
||||
throw new UnsupportedOperationException("Use KyberPublicKeySpec for public key import");
|
||||
}
|
||||
registerPrivateKeyImporter(KyberPrivateKeySpec.class, new PrivateKeyImporter<>() {
|
||||
|
||||
/**
|
||||
* Imports a Kyber private key from a PKCS#8 PrivateKeyInfo encoding.
|
||||
@@ -328,9 +246,14 @@ public final class KyberAlgorithm extends AbstractCryptoAlgorithm {
|
||||
public PrivateKey importPrivate(KyberPrivateKeySpec spec) throws GeneralSecurityException {
|
||||
ensureProvider();
|
||||
KeyFactory kf = KeyFactory.getInstance("Kyber", providerName());
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8()));
|
||||
byte[] encoded = spec.pkcs8();
|
||||
try {
|
||||
return kf.generatePrivate(new PKCS8EncodedKeySpec(encoded));
|
||||
} finally {
|
||||
Arrays.fill(encoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,7 +54,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* <h2>Usage</h2> Instances of this class are passed to the Kyber key builder to
|
||||
* select the desired parameter set: <pre>{@code
|
||||
* CryptoAlgorithm kyber = new KyberAlgorithm();
|
||||
* KeyPair kp = kyber.asymmetricKeyBuilder(KyberKeyGenSpec.class)
|
||||
* KeyPair kp = kyber.asymmetricKeyPairGenerator(KyberKeyGenSpec.class)
|
||||
* .generateKeyPair(KyberKeyGenSpec.kyber768());
|
||||
* }</pre>
|
||||
*
|
||||
@@ -63,7 +63,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* </p>
|
||||
*
|
||||
* @see KyberAlgorithm
|
||||
* @see zeroecho.core.CryptoAlgorithm#generateKeyPair(zeroecho.core.spec.AlgorithmKeySpec)
|
||||
* @see zeroecho.sdk.KeyBuilders.Asymmetric#generateKeyPair(String, AlgorithmKeySpec)
|
||||
*/
|
||||
public final class KyberKeyGenSpec implements AlgorithmKeySpec, Describable {
|
||||
/**
|
||||
|
||||
@@ -33,8 +33,12 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.kyber;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import zeroecho.core.marshal.PairSeq;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
@@ -43,7 +47,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* Specification wrapper for a Kyber (ML-KEM) private key encoded in PKCS#8.
|
||||
*
|
||||
* <p>
|
||||
* Instances of this class carry an immutable copy of the PKCS#8-encoded private
|
||||
* Instances of this class carry an owned copy of the PKCS#8-encoded private
|
||||
* key bytes. They are used with {@link zeroecho.core.CryptoAlgorithm} key
|
||||
* builders to import keys into the provider’s native representation.
|
||||
* </p>
|
||||
@@ -51,7 +55,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* <h2>Encoding</h2>
|
||||
* <ul>
|
||||
* <li>Format: PKCS#8 DER encoding of a Kyber private key.</li>
|
||||
* <li>Stored as a defensive clone to ensure immutability.</li>
|
||||
* <li>Stored as a defensive clone.</li>
|
||||
* <li>Marshalling/unmarshalling supported via {@link PairSeq} with Base64
|
||||
* encoding.</li>
|
||||
* </ul>
|
||||
@@ -60,7 +64,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* // Import a private key into a CryptoAlgorithm
|
||||
* byte[] pkcs8Bytes = ...; // obtained from storage
|
||||
* KyberPrivateKeySpec spec = new KyberPrivateKeySpec(pkcs8Bytes);
|
||||
* PrivateKey k = kyberAlg.importPrivate(spec);
|
||||
* PrivateKey k = kyberAlg.privateKeyImporter(KyberPrivateKeySpec.class).importPrivate(spec);
|
||||
*
|
||||
* // Serialize for persistence
|
||||
* PairSeq seq = KyberPrivateKeySpec.marshal(spec);
|
||||
@@ -70,16 +74,18 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* }</pre>
|
||||
*
|
||||
* <p>
|
||||
* This class is immutable and thread-safe.
|
||||
* Access and destruction are synchronized.
|
||||
* </p>
|
||||
*
|
||||
* @see KyberPublicKeySpec
|
||||
* @see KyberKeyGenSpec
|
||||
*/
|
||||
public final class KyberPrivateKeySpec implements AlgorithmKeySpec {
|
||||
public final class KyberPrivateKeySpec implements AlgorithmKeySpec, Destroyable {
|
||||
|
||||
private static final String PKCS8_B64 = "pkcs8.b64";
|
||||
private final byte[] pkcs8;
|
||||
private final ReentrantLock lifecycleLock = new ReentrantLock();
|
||||
private boolean destroyed;
|
||||
|
||||
/**
|
||||
* Creates a new spec from PKCS#8-encoded private key bytes.
|
||||
@@ -97,7 +103,13 @@ public final class KyberPrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return cloned byte array of PKCS#8 DER encoding
|
||||
*/
|
||||
public byte[] pkcs8() {
|
||||
return pkcs8.clone();
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return pkcs8.clone();
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,7 +126,7 @@ public final class KyberPrivateKeySpec implements AlgorithmKeySpec {
|
||||
* @return key-value representation suitable for persistence
|
||||
*/
|
||||
public static PairSeq marshal(KyberPrivateKeySpec spec) {
|
||||
String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8);
|
||||
String b64 = spec.encodedKey();
|
||||
return PairSeq.of("type", "KyberPrivateKey", PKCS8_B64, b64);
|
||||
}
|
||||
|
||||
@@ -139,7 +151,12 @@ public final class KyberPrivateKeySpec implements AlgorithmKeySpec {
|
||||
if (pkcs8b64 == null) {
|
||||
throw new IllegalArgumentException("KyberPrivateKeySpec: missing 'pkcs8.b64'");
|
||||
}
|
||||
return new KyberPrivateKeySpec(Base64.getDecoder().decode(pkcs8b64));
|
||||
byte[] decoded = Base64.getDecoder().decode(pkcs8b64);
|
||||
try {
|
||||
return new KyberPrivateKeySpec(decoded);
|
||||
} finally {
|
||||
Arrays.fill(decoded, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,4 +172,43 @@ public final class KyberPrivateKeySpec implements AlgorithmKeySpec {
|
||||
public String toString() {
|
||||
return "KyberPrivateKeySpec[len=" + pkcs8.length + "]";
|
||||
}
|
||||
|
||||
private String encodedKey() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
ensureActive();
|
||||
return Base64.getEncoder().withoutPadding().encodeToString(pkcs8);
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
if (!destroyed) {
|
||||
Arrays.fill(pkcs8, (byte) 0);
|
||||
destroyed = true;
|
||||
}
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
lifecycleLock.lock();
|
||||
try {
|
||||
return destroyed;
|
||||
} finally {
|
||||
lifecycleLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureActive() {
|
||||
if (destroyed) {
|
||||
throw new IllegalStateException("Kyber private key specification has been destroyed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
* // Import a public key into a CryptoAlgorithm
|
||||
* byte[] x509Bytes = ...; // obtained from storage
|
||||
* KyberPublicKeySpec spec = new KyberPublicKeySpec(x509Bytes);
|
||||
* PublicKey k = kyberAlg.importPublic(spec);
|
||||
* PublicKey k = kyberAlg.publicKeyImporter(KyberPublicKeySpec.class).importPublic(spec);
|
||||
*
|
||||
* // Serialize for persistence
|
||||
* PairSeq seq = KyberPublicKeySpec.marshal(spec);
|
||||
|
||||
@@ -50,8 +50,9 @@
|
||||
* message-style agreement adapter where needed.</li>
|
||||
* <li>Provide a {@link zeroecho.core.context.KemContext} implementation bound
|
||||
* to either a public or private key for encapsulation or decapsulation.</li>
|
||||
* <li>Expose immutable specifications for key generation variants and encoded
|
||||
* key carriers with compact marshalling helpers.</li>
|
||||
* <li>Expose immutable key-generation specifications and defensively copying
|
||||
* encoded-key carriers with compact marshalling helpers; private-key carriers
|
||||
* are destroyable.</li>
|
||||
* <li>Ensure operations are delegated to an available PQC provider and fail
|
||||
* fast if the provider is absent.</li>
|
||||
* </ul>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user