refactor!: consolidate crypto architecture and security model

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

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

View File

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

View File

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

View File

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

View File

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