security(lib,storage): enforce single-use encryption contexts, encrypt keyring and harden key import
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
14
app/src/test/java/zeroecho/TestKeyringUnlocks.java
Normal file
14
app/src/test/java/zeroecho/TestKeyringUnlocks.java
Normal 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' });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user