security(lib,storage): enforce single-use encryption contexts, encrypt keyring and harden key import

This commit is contained in:
2026-07-29 23:18:22 +02:00
parent 9bbcab7522
commit 8b2f3df41f
64 changed files with 7473 additions and 1799 deletions

View File

@@ -127,7 +127,7 @@ public class GuardTest {
"--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());
int e = Guard.main(encArgs, new Options(), TestKeyringUnlocks.provider());
assertEquals(0, e, "... encrypt expected exit code 0");
// Decrypt (using password)
@@ -135,7 +135,7 @@ public class GuardTest {
"--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());
int d = Guard.main(decArgs, new Options(), TestKeyringUnlocks.provider());
assertEquals(0, d, "... decrypt expected exit code 0");
assertArrayEquals(Files.readAllBytes(in), Files.readAllBytes(dec), "AES-GCM password round-trip mismatch");
@@ -150,7 +150,7 @@ public class GuardTest {
String[] arguments = { "--encrypt", input.toString(), "--to-psw", "controlled", "--alg", "aes-gcm" };
ParseException failure = assertThrows(ParseException.class,
() -> Guard.main(arguments, new Options()));
() -> Guard.main(arguments, new Options(), TestKeyringUnlocks.provider()));
assertTrue(failure.getMessage().contains("--pbkdf2-max"));
System.out.println("...rejected=missingLimits");
@@ -170,7 +170,7 @@ public class GuardTest {
"--alg", "aes-gcm" };
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
() -> Guard.main(arguments, new Options()));
() -> Guard.main(arguments, new Options(), TestKeyringUnlocks.provider()));
assertTrue(failure.getMessage().contains("exactly 16 or 32"));
assertTrue(Files.notExists(output));
@@ -215,14 +215,14 @@ public class GuardTest {
"--to-alias", rsa.pub, "--alg", "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex",
aadAes };
System.out.println("...AES encrypt: " + Arrays.toString(encArgs));
int e = Guard.main(encArgs, new Options());
int e = Guard.main(encArgs, new Options(), TestKeyringUnlocks.provider());
assertEquals(0, e, "... AES encrypt rc");
String[] decArgs = { "--decrypt", enc.toString(), "--output", dec.toString(), "--keyring", ring.toString(),
"--priv-alias", rsa.prv, "--alg", "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex",
aadAes };
System.out.println("...AES decrypt: " + Arrays.toString(decArgs));
int d = Guard.main(decArgs, new Options());
int d = Guard.main(decArgs, new Options(), TestKeyringUnlocks.provider());
assertEquals(0, d, "... AES decrypt rc");
assertArrayEquals(Files.readAllBytes(in), Files.readAllBytes(dec), "RSA AES-GCM round-trip mismatch");
@@ -238,13 +238,13 @@ public class GuardTest {
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--keyring", ring.toString(),
"--to-alias", rsa.pub, "--alg", "chacha-aead", "--aad-hex", aadCha };
System.out.println("...ChaCha encrypt: " + Arrays.toString(encArgs));
int e = Guard.main(encArgs, new Options());
int e = Guard.main(encArgs, new Options(), TestKeyringUnlocks.provider());
assertEquals(0, e, "... ChaCha encrypt rc");
String[] decArgs = { "--decrypt", enc.toString(), "--output", dec.toString(), "--keyring", ring.toString(),
"--priv-alias", rsa.prv, "--alg", "chacha-aead", "--aad-hex", aadCha };
System.out.println("...ChaCha decrypt: " + Arrays.toString(decArgs));
int d = Guard.main(decArgs, new Options());
int d = Guard.main(decArgs, new Options(), TestKeyringUnlocks.provider());
assertEquals(0, d, "... ChaCha decrypt rc");
assertArrayEquals(Files.readAllBytes(in), Files.readAllBytes(dec),
@@ -293,7 +293,7 @@ public class GuardTest {
"--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());
int e = Guard.main(encArgs, new Options(), TestKeyringUnlocks.provider());
assertEquals(0, e, "... encrypt rc");
// Decrypt via private RSA key
@@ -301,7 +301,7 @@ public class GuardTest {
"--priv-alias", rsa.prv, "--alg", "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex",
aad };
System.out.println("...decrypt(private): " + Arrays.toString(decPriv));
int d1 = Guard.main(decPriv, new Options());
int d1 = Guard.main(decPriv, new Options(), TestKeyringUnlocks.provider());
assertEquals(0, d1, "... decrypt(private) rc");
assertArrayEquals(Files.readAllBytes(in), Files.readAllBytes(dec1),
"mixed recipients decrypt(private) mismatch");
@@ -311,7 +311,7 @@ public class GuardTest {
"--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());
int d2 = Guard.main(decPwd, new Options(), TestKeyringUnlocks.provider());
assertEquals(0, d2, "... decrypt(password) rc");
assertArrayEquals(Files.readAllBytes(in), Files.readAllBytes(dec2),
"mixed recipients decrypt(password) mismatch");
@@ -337,14 +337,14 @@ public class GuardTest {
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());
int e = Guard.main(encArgs, new Options(), TestKeyringUnlocks.provider());
assertEquals(0, e, "... encrypt rc");
// Supply both options on purpose
Exception ex = assertThrows(Exception.class, () -> {
String[] bad = { "--decrypt", enc.toString(), "--output", tmp.resolve("out-neg.bin").toString(),
"--password", pwd, "--priv-alias", "whatever", "--alg", "aes-gcm" };
Guard.main(bad, new Options());
Guard.main(bad, new Options(), TestKeyringUnlocks.provider());
});
System.out.println("...got expected exception: " + ex);
System.out.println("...ok");
@@ -377,7 +377,7 @@ public class GuardTest {
String[] genArgs = { "--keystore", ring.toString(), "--generate", "--alg", algId, "--alias", baseAlias,
"--kind", "asym" };
System.out.println("...KeyStoreManagement generate: " + Arrays.toString(genArgs));
int rc = KeyStoreManagement.main(genArgs, new Options());
int rc = KeyStoreManagement.main(genArgs, new Options(), TestKeyringUnlocks.provider());
if (rc != 0) {
throw new GeneralSecurityException("KeyStoreManagement failed with rc=" + rc + " for " + algId);
}

View File

@@ -165,9 +165,12 @@ public class KemTest {
KeyAliases aliases = generateKemIntoKeyStore(ring, kemId, "alias-" + shortId(kemId));
// Sanity: re-open to ensure the file is valid
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);
try (zeroecho.core.storage.KeyringPassword password =
TestKeyringUnlocks.provider().acquire();
KeyringStore ks = KeyringStore.open(ring, password)) {
if (!(ks.contains(aliases.pub) && ks.contains(aliases.prv))) {
throw new IllegalStateException("Keyring does not contain expected aliases for " + kemId);
}
}
// AES-GCM round-trip
@@ -181,7 +184,7 @@ public class KemTest {
int e = Kem.main(new String[] { "--encrypt", plain.toString(), "--output", enc.toString(),
"--keyring", ring.toString(), "--pub", aliases.pub, "--kem", kemId, "--aes", "--aes-cipher",
"gcm", "--aes-tag-bits", Integer.toString(gcmTagBits), "--header", "--aad", aadAes },
new Options());
new Options(), TestKeyringUnlocks.provider());
if (e != 0) {
throw new IllegalStateException("AES encrypt rc=" + e);
}
@@ -189,7 +192,7 @@ public class KemTest {
int d = Kem.main(new String[] { "--decrypt", enc.toString(), "--output", dec.toString(),
"--keyring", ring.toString(), "--priv", aliases.prv, "--kem", kemId, "--aes",
"--aes-cipher", "gcm", "--aes-tag-bits", Integer.toString(gcmTagBits), "--header", "--aad",
aadAes }, new Options());
aadAes }, new Options(), TestKeyringUnlocks.provider());
if (d != 0) {
throw new IllegalStateException("AES decrypt rc=" + d);
}
@@ -208,14 +211,14 @@ public class KemTest {
System.out.println("...[" + kemId + "] ChaCha encrypt");
int e = Kem.main(new String[] { "--encrypt", plain.toString(), "--output", enc.toString(),
"--keyring", ring.toString(), "--pub", aliases.pub, "--kem", kemId, "--chacha",
"--aad", aadChaCha, "--header" }, new Options());
"--aad", aadChaCha, "--header" }, new Options(), TestKeyringUnlocks.provider());
if (e != 0) {
throw new IllegalStateException("ChaCha encrypt rc=" + e);
}
System.out.println("...[" + kemId + "] ChaCha decrypt");
int d = Kem.main(new String[] { "--decrypt", enc.toString(), "--output", dec.toString(),
"--keyring", ring.toString(), "--priv", aliases.prv, "--kem", kemId, "--chacha",
"--aad", aadChaCha, "--header" }, new Options());
"--aad", aadChaCha, "--header" }, new Options(), TestKeyringUnlocks.provider());
if (d != 0) {
throw new IllegalStateException("ChaCha decrypt rc=" + d);
}
@@ -256,7 +259,7 @@ public class KemTest {
ByteArrayOutputStream sink = new ByteArrayOutputStream();
System.setOut(new PrintStream(sink, true, StandardCharsets.UTF_8));
try {
int rc = Kem.main(new String[] { "--list-kems" }, new Options());
int rc = Kem.main(new String[] { "--list-kems" }, new Options(), TestKeyringUnlocks.provider());
if (rc != 0) {
throw new IllegalStateException("--list-kems rc=" + rc);
}
@@ -284,7 +287,7 @@ public class KemTest {
String[] genArgs = { "--keystore", ring.toString(), "--generate", "--alg", kemId, "--alias", baseAlias,
"--kind", "asym" };
System.out.println("...KeyStoreManagement generate: " + Arrays.toString(genArgs));
int rc = KeyStoreManagement.main(genArgs, new Options());
int rc = KeyStoreManagement.main(genArgs, new Options(), TestKeyringUnlocks.provider());
if (rc != 0) {
throw new GeneralSecurityException("KeyStoreManagement failed with rc=" + rc + " for " + kemId);
}

View File

@@ -109,7 +109,7 @@ public class KeyStoreManagementTest {
String[] argv = new String[] { "--keystore", ring.toString(), "--generate", "--alg", id, "--alias",
alias, "--kind", "asym" };
try {
int rc = KeyStoreManagement.main(argv, dispatcher);
int rc = KeyStoreManagement.main(argv, dispatcher, TestKeyringUnlocks.provider());
System.out.println(" rc=" + rc);
assertTrue(rc == 0, "asymmetric generation failed for " + id);
attempted++;
@@ -125,7 +125,7 @@ public class KeyStoreManagementTest {
String[] argv = new String[] { "--keystore", ring.toString(), "--generate", "--alg", id, "--alias",
alias, "--kind", "sym" };
try {
int rc = KeyStoreManagement.main(argv, dispatcher);
int rc = KeyStoreManagement.main(argv, dispatcher, TestKeyringUnlocks.provider());
System.out.println(" rc=" + rc);
assertTrue(rc == 0, "symmetric generation failed for " + id);
attempted++;
@@ -139,7 +139,14 @@ public class KeyStoreManagementTest {
assertTrue(attempted > 0, "No generation attempts were successful");
// Verify by reloading and materializing.
KeyringStore store = KeyringStore.load(session, ring);
zeroecho.core.storage.KeyringPassword password =
TestKeyringUnlocks.provider().acquire();
KeyringStore store;
try {
store = KeyringStore.open(ring, password);
} finally {
password.close();
}
List<String> aliases = store.aliases();
System.out.println("Reloaded aliases (" + aliases.size() + "): " + aliases);
@@ -184,6 +191,7 @@ public class KeyStoreManagementTest {
}
}
assertTrue(ok > 0, "No entries could be materialized back");
store.close();
}
// ---- helpers ----

View File

@@ -127,8 +127,11 @@ public class TagTest {
Path ring = tmp.resolve("ring-ed25519.txt");
KeyAliases ed = generateIntoKeyStore(ring, "Ed25519", "ed");
// sanity
KeyringStore ks = KeyringStore.load(new zeroecho.sdk.ZeroEchoSession(), ring);
assertTrue(ks.contains(ed.pub) && ks.contains(ed.prv), "missing expected aliases");
try (zeroecho.core.storage.KeyringPassword password =
TestKeyringUnlocks.provider().acquire();
KeyringStore ks = KeyringStore.open(ring, password)) {
assertTrue(ks.contains(ed.pub) && ks.contains(ed.prv), "missing expected aliases");
}
byte[] pt = randomBytes(4096);
Path plain = tmp.resolve("plain.bin");
@@ -139,12 +142,12 @@ public class TagTest {
// produce
String[] produce = { "--type", "signature", "--mode", "produce", "--alg", "Ed25519", "--ks", ring.toString(),
"--priv", ed.prv, "--in", plain.toString(), "--out", signed.toString() };
assertEquals(0, Tag.main(produce, new Options()), "produce rc");
assertEquals(0, Tag.main(produce, new Options(), TestKeyringUnlocks.provider()), "produce rc");
// verify (match)
String[] verify = { "--type", "signature", "--mode", "verify", "--alg", "Ed25519", "--ks", ring.toString(),
"--pub", ed.pub, "--in", signed.toString(), "--out", recovered.toString() };
assertEquals(0, Tag.main(verify, new Options()), "verify rc");
assertEquals(0, Tag.main(verify, new Options(), TestKeyringUnlocks.provider()), "verify rc");
assertArrayEquals(pt, Files.readAllBytes(recovered), "round-trip mismatch");
@@ -168,7 +171,7 @@ public class TagTest {
assertEquals(0,
Tag.main(new String[] { "--type", "signature", "--mode", "produce", "--alg", "Ed25519", "--ks",
ring.toString(), "--priv", ed.prv, "--in", plain.toString(), "--out", signed.toString() },
new Options()));
new Options(), TestKeyringUnlocks.provider()));
// corrupt last byte -> break signature
flipLastByte(signed);
@@ -178,7 +181,7 @@ public class TagTest {
Tag.main(
new String[] { "--type", "signature", "--mode", "verify", "--alg", "Ed25519", "--ks",
ring.toString(), "--pub", ed.pub, "--in", signed.toString(), "--out", out.toString() },
new Options()));
new Options(), TestKeyringUnlocks.provider()));
assertTrue(Files.notExists(out, LinkOption.NOFOLLOW_LINKS));
@@ -198,11 +201,11 @@ public class TagTest {
// produce
assertEquals(0, Tag.main(new String[] { "--type", "digest", "--mode", "produce", "--alg", "SHA-256", "--in",
plain.toString(), "--out", tagged.toString() }, new Options()));
plain.toString(), "--out", tagged.toString() }, new Options(), TestKeyringUnlocks.provider()));
// verify (match)
assertEquals(0, Tag.main(new String[] { "--type", "digest", "--mode", "verify", "--alg", "SHA-256", "--in",
tagged.toString(), "--out", recovered.toString() }, new Options()));
tagged.toString(), "--out", recovered.toString() }, new Options(), TestKeyringUnlocks.provider()));
assertArrayEquals(pt, Files.readAllBytes(recovered), "digest round-trip mismatch");
@@ -221,14 +224,14 @@ public class TagTest {
// produce
assertEquals(0, Tag.main(new String[] { "--type", "digest", "--mode", "produce", "--alg", "SHA-256", "--in",
plain.toString(), "--out", tagged.toString() }, new Options()));
plain.toString(), "--out", tagged.toString() }, new Options(), TestKeyringUnlocks.provider()));
// corrupt last byte -> break digest
flipLastByte(tagged);
// verify (mismatch): expect throw + default marker ("digest invalid")
assertEquals(1, Tag.main(new String[] { "--type", "digest", "--mode", "verify", "--alg", "SHA-256", "--in",
tagged.toString(), "--out", out.toString() }, new Options()));
tagged.toString(), "--out", out.toString() }, new Options(), TestKeyringUnlocks.provider()));
assertTrue(Files.notExists(out, LinkOption.NOFOLLOW_LINKS));
@@ -252,7 +255,7 @@ public class TagTest {
assertEquals(0, Tag.main(
new String[] { "--type", "digest", "--mode", "produce", "--alg", "SHA-256", "--in", "-", "--out", "-" },
new Options()));
new Options(), TestKeyringUnlocks.provider()));
// save produced bytes
Path tagged = tmp.resolve("stdio-tagged.bin");
@@ -264,7 +267,7 @@ public class TagTest {
System.setOut(new PrintStream(verifiedSink, true, StandardCharsets.UTF_8));
assertEquals(0, Tag.main(new String[] { "--type", "digest", "--mode", "verify", "--alg", "SHA-256", "--in",
tagged.toString(), "--out", "-" }, new Options()));
tagged.toString(), "--out", "-" }, new Options(), TestKeyringUnlocks.provider()));
assertArrayEquals(pt, verifiedSink.toByteArray(), "stdio round-trip mismatch");
@@ -279,7 +282,7 @@ public class TagTest {
private static KeyAliases generateIntoKeyStore(Path ring, String algId, String baseAlias) throws Exception {
String[] genArgs = { "--keystore", ring.toString(), "--generate", "--alg", algId, "--alias", baseAlias,
"--kind", "asym" };
int rc = KeyStoreManagement.main(genArgs, new Options());
int rc = KeyStoreManagement.main(genArgs, new Options(), TestKeyringUnlocks.provider());
if (rc != 0) {
throw new GeneralSecurityException("KeyStoreManagement failed with rc=" + rc + " for " + algId);
}

View File

@@ -0,0 +1,14 @@
package zeroecho;
import zeroecho.core.spi.KeyringUnlockProvider;
import zeroecho.core.storage.KeyringPassword;
final class TestKeyringUnlocks {
private TestKeyringUnlocks() {
}
static KeyringUnlockProvider provider() {
return () -> new KeyringPassword(
new char[] { 't', 'e', 's', 't', '-', 'k', 'e', 'y', 'r', 'i', 'n', 'g' });
}
}