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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user