Initial commit (history reset)
This commit is contained in:
148
app/src/test/java/zeroecho/CovertCommandTest.java
Normal file
148
app/src/test/java/zeroecho/CovertCommandTest.java
Normal file
@@ -0,0 +1,148 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.
|
||||
******************************************************************************/
|
||||
package zeroecho;
|
||||
|
||||
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.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
import org.apache.commons.cli.Options;
|
||||
import org.apache.commons.cli.ParseException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
public class CovertCommandTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private Path copyTestJpeg(String name) throws IOException {
|
||||
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(name)) {
|
||||
if (inputStream == null) {
|
||||
throw new IllegalArgumentException("Missing resource: " + name);
|
||||
}
|
||||
|
||||
Path target = tempDir.resolve(name);
|
||||
Files.copy(inputStream, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmbedAndExtractDefaultSlots() throws Exception {
|
||||
System.out.println("testEmbedAndExtractDefaultSlots");
|
||||
|
||||
// Prepare input JPEG and payload
|
||||
Path jpeg = copyTestJpeg("test.jpg");
|
||||
Path payload = tempDir.resolve("secret.txt");
|
||||
Path stegoOutput = tempDir.resolve("stego.jpg");
|
||||
Path extractedOutput = tempDir.resolve("extracted.dat");
|
||||
|
||||
String message = "SecretMessage123!";
|
||||
Files.write(payload, message.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// --- Embed ---
|
||||
Options embedOptions = new Options();
|
||||
int embedCode = CovertCommand.main(new String[] { "--embed", "--jpeg", jpeg.toString(), "--payload",
|
||||
payload.toString(), "--output", stegoOutput.toString() }, embedOptions);
|
||||
assertEquals(0, embedCode);
|
||||
assertTrue(Files.exists(stegoOutput));
|
||||
assertTrue(Files.size(stegoOutput) > Files.size(jpeg));
|
||||
|
||||
// --- Extract ---
|
||||
Options extractOptions = new Options();
|
||||
int extractCode = CovertCommand.main(
|
||||
new String[] { "--extract", "--jpeg", stegoOutput.toString(), "--output", extractedOutput.toString() },
|
||||
extractOptions);
|
||||
assertEquals(0, extractCode);
|
||||
assertTrue(Files.exists(extractedOutput));
|
||||
|
||||
// Verify content
|
||||
String extracted = Files.readString(extractedOutput);
|
||||
assertEquals(message, extracted);
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmbedFailsWithoutPayload() {
|
||||
System.out.println("testEmbedFailsWithoutPayload");
|
||||
|
||||
Exception thrown = assertThrows(ParseException.class, () -> {
|
||||
Options options = new Options();
|
||||
CovertCommand.main(new String[] { "--embed", "--jpeg", "input.jpg", "--output", "out.jpg" }, options);
|
||||
});
|
||||
|
||||
assertTrue(thrown.getMessage().contains("--payload is required"));
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomSlotsEmbedding() throws Exception {
|
||||
System.out.println("testCustomSlotsEmbedding");
|
||||
|
||||
Path jpeg = copyTestJpeg("test.jpg");
|
||||
Path payload = tempDir.resolve("msg.bin");
|
||||
Path stego = tempDir.resolve("custom_stego.jpg");
|
||||
Path extracted = tempDir.resolve("custom_extracted.bin");
|
||||
|
||||
String secret = "This uses custom slots!";
|
||||
Files.write(payload, secret.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Correctly defined custom slot string
|
||||
String slotString = "Exif.UserComment:2048;Exif.CustomDesc/tag=700,ascii,64,exif:1024";
|
||||
|
||||
Options embedOptions = new Options();
|
||||
int code = CovertCommand.main(new String[] { "--embed", "--jpeg", jpeg.toString(), "--payload",
|
||||
payload.toString(), "--output", stego.toString(), "--slots", slotString }, embedOptions);
|
||||
assertEquals(0, code);
|
||||
assertTrue(Files.exists(stego));
|
||||
|
||||
Options extractOptions = new Options();
|
||||
code = CovertCommand.main(new String[] { "--extract", "--jpeg", stego.toString(), "--output",
|
||||
extracted.toString(), "--slots", slotString }, extractOptions);
|
||||
assertEquals(0, code);
|
||||
|
||||
String extractedMsg = Files.readString(extracted);
|
||||
assertEquals(secret, extractedMsg);
|
||||
System.out.println("...ok");
|
||||
}
|
||||
}
|
||||
354
app/src/test/java/zeroecho/GuardTest.java
Normal file
354
app/src/test/java/zeroecho/GuardTest.java
Normal file
@@ -0,0 +1,354 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.
|
||||
******************************************************************************/
|
||||
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 java.io.PrintStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
import org.apache.commons.cli.Options;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
/**
|
||||
* CLI-level round-trip tests for the Guard subcommand.
|
||||
*
|
||||
* <h2>Scope</h2> These tests drive
|
||||
* {@link Guard#main(String[], org.apache.commons.cli.Options)} with only the
|
||||
* options implemented by Guard. They exercise:
|
||||
* <ul>
|
||||
* <li>Password-only encryption and decryption,</li>
|
||||
* <li>RSA recipients with private-key based decryption,</li>
|
||||
* <li>Mixed recipients (RSA + ElGamal + password) with decoys and default
|
||||
* shuffling,</li>
|
||||
* <li>AES-GCM (with tag bits and AAD) and ChaCha20-Poly1305 (with AAD and
|
||||
* explicit nonce).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Keys are generated using the existing KeyStoreManagement CLI to populate a
|
||||
* real KeyringStore file, as done in KemTest. The CLI persists aliases as
|
||||
* {@code <alias>.pub} and {@code <alias>.prv}.
|
||||
* </p>
|
||||
*/
|
||||
public class GuardTest {
|
||||
|
||||
/** All temporary files live here and are auto-cleaned by JUnit. */
|
||||
@TempDir
|
||||
Path tmp;
|
||||
|
||||
private PrintStream savedOut;
|
||||
|
||||
@BeforeAll
|
||||
static void bootBouncyCastle() {
|
||||
System.out.println("bootBouncyCastle()");
|
||||
// The project initializes BC explicitly where needed.
|
||||
BouncyCastleActivator.init();
|
||||
System.out.println("bootBouncyCastle...ok");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void restoreStdout() {
|
||||
if (savedOut != null) {
|
||||
System.setOut(savedOut);
|
||||
savedOut = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Password-only round trip using AES-GCM with header and AAD.
|
||||
*
|
||||
* <p>
|
||||
* This does not require a keyring.
|
||||
* </p>
|
||||
*/
|
||||
@Test
|
||||
void password_aesGcm_roundTrip_ok() throws Exception {
|
||||
final String method = "password_aesGcm_roundTrip_ok()";
|
||||
final String password = "Tr0ub4dor&3";
|
||||
final int size = 4096;
|
||||
final String aadHex = "A1B2C3D4";
|
||||
final int tagBits = 128;
|
||||
|
||||
System.out.println(method);
|
||||
System.out.println("...params: size=" + size + " tagBits=" + tagBits + " aadHex=" + aadHex);
|
||||
|
||||
Path in = writeRandom(tmp.resolve("pt.bin"), size, 0xA11CE01);
|
||||
Path enc = tmp.resolve("pt.bin.enc");
|
||||
Path dec = tmp.resolve("pt.bin.dec");
|
||||
|
||||
// Encrypt
|
||||
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", password, "--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",
|
||||
"aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aadHex };
|
||||
System.out.println("...decrypt: " + Arrays.toString(decArgs));
|
||||
int d = Guard.main(decArgs, new Options());
|
||||
assertEquals(0, d, "... decrypt expected exit code 0");
|
||||
|
||||
assertArrayEquals(Files.readAllBytes(in), Files.readAllBytes(dec), "AES-GCM password round-trip mismatch");
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA recipient round trips with both AES-GCM and ChaCha20-Poly1305 payloads.
|
||||
*
|
||||
* <p>
|
||||
* Keys are generated through the real KeyStoreManagement CLI and read via Guard
|
||||
* with --to-alias and --priv-alias.
|
||||
* </p>
|
||||
*/
|
||||
@Test
|
||||
void rsa_alias_aesGcm_and_chacha_roundTrip_ok() throws Exception {
|
||||
final String method = "rsa_alias_aesGcm_and_chacha_roundTrip_ok()";
|
||||
final String base = "alice";
|
||||
final String rsaId = "RSA";
|
||||
final int sizeAes = 8192;
|
||||
final int sizeCha = 3072;
|
||||
final int tagBits = 128;
|
||||
final String aadAes = "010203";
|
||||
final String aadCha = "D00DFEED";
|
||||
final String chNonce = "00112233445566778899AABB";
|
||||
|
||||
System.out.println(method);
|
||||
System.out.println("...params: sizeAes=" + sizeAes + " sizeCha=" + sizeCha + " tagBits=" + tagBits + " aadAes="
|
||||
+ aadAes + " aadCha=" + aadCha + " chNonce=" + chNonce);
|
||||
|
||||
// Prepare keyring with RSA pair
|
||||
Path ring = tmp.resolve("ring-rsa.txt");
|
||||
KeyAliases rsa = generateIntoKeyStore(ring, rsaId, base);
|
||||
|
||||
// AES-GCM round-trip
|
||||
{
|
||||
Path in = writeRandom(tmp.resolve("rsa-pt-aes.bin"), sizeAes, 0x5157A11);
|
||||
Path enc = tmp.resolve("rsa-pt-aes.bin.enc");
|
||||
Path dec = tmp.resolve("rsa-pt-aes.bin.dec");
|
||||
|
||||
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--keyring", ring.toString(),
|
||||
"--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());
|
||||
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());
|
||||
assertEquals(0, d, "... AES decrypt rc");
|
||||
|
||||
assertArrayEquals(Files.readAllBytes(in), Files.readAllBytes(dec), "RSA AES-GCM round-trip mismatch");
|
||||
System.out.println("...AES round-trip ok");
|
||||
}
|
||||
|
||||
// ChaCha20-Poly1305 round-trip
|
||||
{
|
||||
Path in = writeRandom(tmp.resolve("rsa-pt-ch.bin"), sizeCha, 0xC0FFEE1);
|
||||
Path enc = tmp.resolve("rsa-pt-ch.bin.enc");
|
||||
Path dec = tmp.resolve("rsa-pt-ch.bin.dec");
|
||||
|
||||
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--keyring", ring.toString(),
|
||||
"--to-alias", rsa.pub, "--alg", "chacha-aead", "--aad-hex", aadCha, "--nonce-hex", chNonce };
|
||||
System.out.println("...ChaCha encrypt: " + Arrays.toString(encArgs));
|
||||
int e = Guard.main(encArgs, new Options());
|
||||
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, "--nonce-hex", chNonce };
|
||||
System.out.println("...ChaCha decrypt: " + Arrays.toString(decArgs));
|
||||
int d = Guard.main(decArgs, new Options());
|
||||
assertEquals(0, d, "... ChaCha decrypt rc");
|
||||
|
||||
assertArrayEquals(Files.readAllBytes(in), Files.readAllBytes(dec),
|
||||
"RSA ChaCha20-Poly1305 round-trip mismatch");
|
||||
System.out.println("...ChaCha round-trip ok");
|
||||
}
|
||||
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixed recipients with decoys: RSA + password recipients plus an ElGamal decoy
|
||||
* alias.
|
||||
*
|
||||
* <p>
|
||||
* Verifies that default shuffling does not prevent decryption and that both
|
||||
* password and private-key paths can unlock.
|
||||
* </p>
|
||||
*/
|
||||
@Test
|
||||
void mixed_recipients_with_decoys_roundTrip_ok() throws Exception {
|
||||
final String method = "mixed_recipients_with_decoys_roundTrip_ok()";
|
||||
final int size = 4096;
|
||||
|
||||
System.out.println(method);
|
||||
|
||||
// Prepare keyring with RSA and ElGamal
|
||||
Path ring = tmp.resolve("ring-mixed.txt");
|
||||
KeyAliases rsa = generateIntoKeyStore(ring, "RSA", "bob");
|
||||
KeyAliases elg = generateIntoKeyStore(ring, "ElGamal", "carol"); // used as decoy alias
|
||||
|
||||
Path in = writeRandom(tmp.resolve("pt-mixed.bin"), size, 0xBADC0DE);
|
||||
Path enc = tmp.resolve("pt-mixed.bin.enc");
|
||||
Path dec1 = tmp.resolve("pt-mixed.bin.dec1");
|
||||
Path dec2 = tmp.resolve("pt-mixed.bin.dec2");
|
||||
|
||||
final String password = "correct horse battery staple";
|
||||
final String aad = "FEEDFACE";
|
||||
final int tagBits = 128;
|
||||
|
||||
// Encrypt with: RSA real recipient, password real recipient, ElGamal decoy
|
||||
// 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",
|
||||
"aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aad };
|
||||
System.out.println("...encrypt: " + Arrays.toString(encArgs));
|
||||
int e = Guard.main(encArgs, new Options());
|
||||
assertEquals(0, e, "... encrypt rc");
|
||||
|
||||
// Decrypt via private RSA key
|
||||
String[] decPriv = { "--decrypt", enc.toString(), "--output", dec1.toString(), "--keyring", ring.toString(),
|
||||
"--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());
|
||||
assertEquals(0, d1, "... decrypt(private) rc");
|
||||
assertArrayEquals(Files.readAllBytes(in), Files.readAllBytes(dec1),
|
||||
"mixed recipients decrypt(private) mismatch");
|
||||
|
||||
// Decrypt via password instead of key
|
||||
String[] decPwd = { "--decrypt", enc.toString(), "--output", dec2.toString(), "--password", password, "--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());
|
||||
assertEquals(0, d2, "... decrypt(password) rc");
|
||||
assertArrayEquals(Files.readAllBytes(in), Files.readAllBytes(dec2),
|
||||
"mixed recipients decrypt(password) mismatch");
|
||||
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
/**
|
||||
* Negative: decryption should fail to parse when both --priv-alias and
|
||||
* --password are given.
|
||||
*/
|
||||
@Test
|
||||
void decrypt_with_both_unlock_material_rejected() throws Exception {
|
||||
final String method = "decrypt_with_both_unlock_material_rejected()";
|
||||
System.out.println(method);
|
||||
|
||||
// Minimal valid blob: encrypt with password, then try to decrypt specifying
|
||||
// both unlock options
|
||||
Path in = writeRandom(tmp.resolve("pt-neg.bin"), 256, 0xABCD);
|
||||
Path enc = tmp.resolve("pt-neg.bin.enc");
|
||||
String pwd = "x";
|
||||
|
||||
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", pwd, "--alg",
|
||||
"aes-gcm", "--tag-bits", "128" };
|
||||
int e = Guard.main(encArgs, new Options());
|
||||
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());
|
||||
});
|
||||
System.out.println("...got expected exception: " + ex);
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
private static Path writeRandom(Path p, int size, long seed) throws Exception {
|
||||
byte[] b = new byte[size];
|
||||
new Random(seed).nextBytes(b);
|
||||
Files.write(p, b);
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an asymmetric keypair using the KeyStoreManagement CLI.
|
||||
*
|
||||
* <p>
|
||||
* The CLI stores aliases as {@code <alias>.pub} and {@code <alias>.prv}.
|
||||
* </p>
|
||||
*
|
||||
* @param ring keyring file path
|
||||
* @param algId algorithm id, e.g. "RSA", "ElGamal", "ML-KEM"
|
||||
* @param baseAlias base alias without suffix
|
||||
* @return public/private aliases to be used with Guard
|
||||
*/
|
||||
private static KeyAliases generateIntoKeyStore(Path ring, String algId, String baseAlias) throws Exception {
|
||||
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());
|
||||
if (rc != 0) {
|
||||
throw new GeneralSecurityException("KeyStoreManagement failed with rc=" + rc + " for " + algId);
|
||||
}
|
||||
return new KeyAliases(baseAlias + ".pub", baseAlias + ".prv");
|
||||
}
|
||||
|
||||
private static final class KeyAliases {
|
||||
final String pub;
|
||||
final String prv;
|
||||
|
||||
KeyAliases(String pub, String prv) {
|
||||
this.pub = pub;
|
||||
this.prv = prv;
|
||||
}
|
||||
}
|
||||
}
|
||||
319
app/src/test/java/zeroecho/KemTest.java
Normal file
319
app/src/test/java/zeroecho/KemTest.java
Normal file
@@ -0,0 +1,319 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.
|
||||
******************************************************************************/
|
||||
package zeroecho;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import org.apache.commons.cli.Options;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import zeroecho.core.storage.KeyringStore;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
/**
|
||||
* CLI-level tests for KEMAes that:
|
||||
* <ul>
|
||||
* <li>Verify {@code --list-kems} runs without a keyring.</li>
|
||||
* <li>Iterate over <em>all</em> available KEM ids discovered via
|
||||
* {@code --list-kems} and for each id perform hybrid round-trips:
|
||||
* <ul>
|
||||
* <li>AES-GCM with header and AAD,</li>
|
||||
* <li>ChaCha20-Poly1305 with header and AAD.</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The tests:
|
||||
* <ul>
|
||||
* <li>use the real KeyStore format by calling {@link KeyStoreManagement} to
|
||||
* generate a keypair,</li>
|
||||
* <li>pass the resulting aliases into
|
||||
* {@code KEMAes.main(String[], Options)},</li>
|
||||
* <li>print the method name and parameters first, progress lines prefixed by
|
||||
* {@code "..."},</li>
|
||||
* <li>end with {@code "...ok"} on success.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class KemTest {
|
||||
|
||||
/** All temporary files live here and are auto-cleaned by JUnit. */
|
||||
@TempDir
|
||||
Path tmp;
|
||||
|
||||
private PrintStream savedOut;
|
||||
|
||||
@BeforeAll
|
||||
static void bootBouncyCastle() {
|
||||
System.out.println("bootBouncyCastle()");
|
||||
// The project initializes BC explicitly for KEM implementations.
|
||||
BouncyCastleActivator.init();
|
||||
System.out.println("bootBouncyCastle...ok");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void restoreStdout() {
|
||||
if (savedOut != null) {
|
||||
System.setOut(savedOut);
|
||||
savedOut = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms that {@code --list-kems} short-circuits and exits 0 without
|
||||
* requiring a keyring.
|
||||
*/
|
||||
@Test
|
||||
void listKems_runs() throws Exception {
|
||||
System.out.println("listKems_runs()");
|
||||
Options opts = new Options();
|
||||
String[] args = { "--list-kems" };
|
||||
|
||||
System.out.println("...invoking: " + Arrays.toString(args));
|
||||
int rc = Kem.main(args, opts);
|
||||
assertEquals(0, rc, "... expected exit code 0");
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs hybrid round-trips for <em>every</em> KEM listed by {@code --list-kems}:
|
||||
* <ol>
|
||||
* <li>Generate a real KeyStore with a fresh keypair for the KEM,</li>
|
||||
* <li>Encrypt+decrypt using AES-GCM (header+AAD),</li>
|
||||
* <li>Encrypt+decrypt using ChaCha20-Poly1305 (header+AAD).</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>
|
||||
* If any KEM fails at any step, the test records it and continues. At the end,
|
||||
* it fails with a concise summary of all failures.
|
||||
* </p>
|
||||
*/
|
||||
@Test
|
||||
void allKems_encryptDecrypt_aesAndChacha() throws Exception {
|
||||
final String method = "allKems_encryptDecrypt_aesAndChacha()";
|
||||
final int aesSize = 8192;
|
||||
final int chachaSize = 4096;
|
||||
final int gcmTagBits = 128;
|
||||
final String aadAes = "A1B2C3";
|
||||
final String aadChaCha = "DEADBEEF";
|
||||
final String nonceChaCha = "00112233445566778899AABB";
|
||||
|
||||
System.out.println(method);
|
||||
System.out.println("...params: aesSize=" + aesSize + " chachaSize=" + chachaSize + " gcmTagBits=" + gcmTagBits
|
||||
+ " aesAAD=" + aadAes + " chachaAAD=" + aadChaCha + " chachaNonce=" + nonceChaCha);
|
||||
|
||||
// Discover KEM ids via the CLI (ensures we use exactly the ids users will see).
|
||||
List<String> kemIds = listKemsViaCli();
|
||||
System.out.println("...discovered " + kemIds.size() + " KEM ids");
|
||||
if (kemIds.isEmpty()) {
|
||||
throw new GeneralSecurityException("No KEM algorithms reported by --list-kems");
|
||||
}
|
||||
|
||||
List<String> failures = new ArrayList<>();
|
||||
|
||||
for (String kemId : kemIds) {
|
||||
System.out.println("...KEM " + kemId + " begin");
|
||||
|
||||
try {
|
||||
// Real keystore for this KEM
|
||||
Path ring = tmp.resolve("ring-" + kemId.replace('/', '_') + ".txt");
|
||||
KeyAliases aliases = generateKemIntoKeyStore(ring, kemId, "alias-" + shortId(kemId));
|
||||
|
||||
// Sanity: re-open to ensure the file is valid
|
||||
KeyringStore ks = KeyringStore.load(ring);
|
||||
if (!(ks.contains(aliases.pub) && ks.contains(aliases.prv))) {
|
||||
throw new IllegalStateException("Keyring does not contain expected aliases for " + kemId);
|
||||
}
|
||||
|
||||
// AES-GCM round-trip
|
||||
{
|
||||
byte[] content = randomBytes(aesSize);
|
||||
Path plain = tmp.resolve("plain-aes-" + shortId(kemId) + ".bin");
|
||||
Path enc = tmp.resolve("enc-aes-" + shortId(kemId) + ".bin");
|
||||
Path dec = tmp.resolve("dec-aes-" + shortId(kemId) + ".bin");
|
||||
Files.write(plain, content);
|
||||
System.out.println("...[" + kemId + "] AES encrypt");
|
||||
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());
|
||||
if (e != 0) {
|
||||
throw new IllegalStateException("AES encrypt rc=" + e);
|
||||
}
|
||||
System.out.println("...[" + kemId + "] AES decrypt");
|
||||
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());
|
||||
if (d != 0) {
|
||||
throw new IllegalStateException("AES decrypt rc=" + d);
|
||||
}
|
||||
byte[] back = Files.readAllBytes(dec);
|
||||
assertArrayEquals(content, back, "[" + kemId + "] AES-GCM round-trip mismatch");
|
||||
System.out.println("...[" + kemId + "] AES round-trip ok");
|
||||
}
|
||||
|
||||
// ChaCha20-Poly1305 round-trip (AEAD implied by AAD)
|
||||
{
|
||||
byte[] content = randomBytes(chachaSize);
|
||||
Path plain = tmp.resolve("plain-cc20-" + shortId(kemId) + ".bin");
|
||||
Path enc = tmp.resolve("enc-cc20-" + shortId(kemId) + ".bin");
|
||||
Path dec = tmp.resolve("dec-cc20-" + shortId(kemId) + ".bin");
|
||||
Files.write(plain, content);
|
||||
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",
|
||||
"--chacha-nonce", nonceChaCha, "--aad", aadChaCha, "--header" }, new Options());
|
||||
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",
|
||||
"--chacha-nonce", nonceChaCha, "--aad", aadChaCha, "--header" }, new Options());
|
||||
if (d != 0) {
|
||||
throw new IllegalStateException("ChaCha decrypt rc=" + d);
|
||||
}
|
||||
byte[] back = Files.readAllBytes(dec);
|
||||
assertArrayEquals(content, back, "[" + kemId + "] ChaCha20-Poly1305 round-trip mismatch");
|
||||
System.out.println("...[" + kemId + "] ChaCha round-trip ok");
|
||||
}
|
||||
|
||||
System.out.println("...KEM " + kemId + " ok");
|
||||
} catch (Throwable t) {
|
||||
System.out.println("...KEM " + kemId + " FAILED: " + t);
|
||||
failures.add(kemId + " -> " + t.getClass().getSimpleName() + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (!failures.isEmpty()) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Some KEM(s) failed:\n");
|
||||
for (String f : failures) {
|
||||
sb.append(" - ").append(f).append('\n');
|
||||
}
|
||||
throw new AssertionError(sb.toString());
|
||||
}
|
||||
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Calls the CLI entrypoint with {@code --list-kems} and parses stdout lines to
|
||||
* a list of ids.
|
||||
*/
|
||||
private List<String> listKemsViaCli() throws Exception {
|
||||
savedOut = System.out;
|
||||
ByteArrayOutputStream sink = new ByteArrayOutputStream();
|
||||
System.setOut(new PrintStream(sink, true, StandardCharsets.UTF_8));
|
||||
try {
|
||||
int rc = Kem.main(new String[] { "--list-kems" }, new Options());
|
||||
if (rc != 0) {
|
||||
throw new IllegalStateException("--list-kems rc=" + rc);
|
||||
}
|
||||
} finally {
|
||||
System.setOut(savedOut);
|
||||
savedOut = null;
|
||||
}
|
||||
String out = sink.toString(StandardCharsets.UTF_8);
|
||||
List<String> ids = new ArrayList<>();
|
||||
for (String line : out.split("\\R")) {
|
||||
String id = line.trim();
|
||||
if (!id.isEmpty() && !id.startsWith("(")) {
|
||||
ids.add(id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a KEM keypair using the real KeyStoreManagement CLI and returns the
|
||||
* public/private aliases. The CLI stores aliases as {@code <alias>.pub} and
|
||||
* {@code <alias>.prv}.
|
||||
*/
|
||||
private static KeyAliases generateKemIntoKeyStore(Path ring, String kemId, String baseAlias) throws Exception {
|
||||
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());
|
||||
if (rc != 0) {
|
||||
throw new GeneralSecurityException("KeyStoreManagement failed with rc=" + rc + " for " + kemId);
|
||||
}
|
||||
return new KeyAliases(baseAlias + ".pub", baseAlias + ".prv");
|
||||
}
|
||||
|
||||
private static String shortId(String kemId) {
|
||||
String s = kemId.replaceAll("[^A-Za-z0-9]+", "");
|
||||
if (s.length() > 16) {
|
||||
s = s.substring(0, 16);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private static byte[] randomBytes(int n) {
|
||||
byte[] b = new byte[n];
|
||||
new Random(0x5EEDC0DEL).nextBytes(b);
|
||||
return b;
|
||||
}
|
||||
|
||||
private static final class KeyAliases {
|
||||
final String pub;
|
||||
final String prv;
|
||||
|
||||
KeyAliases(String pub, String prv) {
|
||||
this.pub = pub;
|
||||
this.prv = prv;
|
||||
}
|
||||
}
|
||||
}
|
||||
241
app/src/test/java/zeroecho/KeyStoreManagementTest.java
Normal file
241
app/src/test/java/zeroecho/KeyStoreManagementTest.java
Normal file
@@ -0,0 +1,241 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.
|
||||
******************************************************************************/
|
||||
package zeroecho;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.apache.commons.cli.Options;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
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.storage.KeyringStore;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
/**
|
||||
* KeyStoreManagementTest drives the KeyStoreManagement CLI across all available
|
||||
* algorithms, printing progress and verifying that generated entries can be
|
||||
* materialized via KeyringStore.
|
||||
*
|
||||
* <h2>What it does</h2>
|
||||
* <ul>
|
||||
* <li>For each algorithm that exposes a default-capable asymmetric builder,
|
||||
* generates a keypair.</li>
|
||||
* <li>For each algorithm that exposes a default-capable symmetric builder,
|
||||
* generates a secret.</li>
|
||||
* <li>Reloads the keyring and materializes keys using KeyringStore.</li>
|
||||
* <li>Prints progress and brief details to stdout during the run.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class KeyStoreManagementTest {
|
||||
|
||||
@TempDir
|
||||
Path tmp;
|
||||
|
||||
@BeforeAll
|
||||
static void setupProviders() {
|
||||
BouncyCastleActivator.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates keypairs and secrets where supported and verifies materialization.
|
||||
*
|
||||
* @throws Exception on unexpected failure
|
||||
*/
|
||||
@Test
|
||||
public void generateAndVerifyAllAlgorithms() throws Exception {
|
||||
Path ring = tmp.resolve("ring.txt");
|
||||
|
||||
Set<String> algIds = CryptoAlgorithms.available();
|
||||
System.out.println("Algorithms: " + algIds);
|
||||
assertTrue(!algIds.isEmpty(), "Catalog should not be empty");
|
||||
|
||||
int attempted = 0;
|
||||
for (String id : algIds) {
|
||||
Options dispatcher = new Options();
|
||||
CryptoAlgorithm alg = CryptoAlgorithms.require(id);
|
||||
boolean doAsym = hasAsymmetricDefault(alg);
|
||||
boolean doSym = hasSymmetricDefault(alg);
|
||||
|
||||
if (doAsym) {
|
||||
String alias = "asym-" + sanitize(id) + "-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
System.out.println("Generating asymmetric: " + id + " -> " + alias + ".pub/.prv");
|
||||
String[] argv = new String[] { "--keystore", ring.toString(), "--generate", "--alg", id, "--alias",
|
||||
alias, "--kind", "asym" };
|
||||
try {
|
||||
int rc = KeyStoreManagement.main(argv, dispatcher);
|
||||
System.out.println(" rc=" + rc);
|
||||
assertTrue(rc == 0, "asymmetric generation failed for " + id);
|
||||
attempted++;
|
||||
} catch (Throwable t) {
|
||||
System.out.println(
|
||||
" SKIP asym " + id + " due to " + t.getClass().getSimpleName() + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (doSym) {
|
||||
String alias = "sym-" + sanitize(id) + "-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
System.out.println("Generating symmetric: " + id + " -> " + alias);
|
||||
String[] argv = new String[] { "--keystore", ring.toString(), "--generate", "--alg", id, "--alias",
|
||||
alias, "--kind", "sym" };
|
||||
try {
|
||||
int rc = KeyStoreManagement.main(argv, dispatcher);
|
||||
System.out.println(" rc=" + rc);
|
||||
assertTrue(rc == 0, "symmetric generation failed for " + id);
|
||||
attempted++;
|
||||
} catch (Throwable t) {
|
||||
System.out.println(
|
||||
" SKIP sym " + id + " due to " + t.getClass().getSimpleName() + ": " + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(attempted > 0, "No generation attempts were successful");
|
||||
|
||||
// Verify by reloading and materializing.
|
||||
KeyringStore store = KeyringStore.load(ring);
|
||||
List<String> aliases = store.aliases();
|
||||
System.out.println("Reloaded aliases (" + aliases.size() + "): " + aliases);
|
||||
|
||||
int ok = 0;
|
||||
for (int i = 0; i < aliases.size(); i++) {
|
||||
String a = aliases.get(i);
|
||||
boolean good = false;
|
||||
try {
|
||||
PublicKey pub = store.getPublic(a);
|
||||
if (pub != null) {
|
||||
System.out.println(" OK public: " + a + " alg=" + pub.getAlgorithm() + " encLen="
|
||||
+ encLen(pub.getEncoded()));
|
||||
good = true;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
if (!good) {
|
||||
try {
|
||||
PrivateKey prv = store.getPrivate(a);
|
||||
if (prv != null) {
|
||||
System.out.println(" OK private: " + a + " alg=" + prv.getAlgorithm() + " encLen="
|
||||
+ encLen(prv.getEncoded()));
|
||||
good = true;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
if (!good) {
|
||||
try {
|
||||
SecretKey sk = store.getSecret(a);
|
||||
if (sk != null) {
|
||||
byte[] raw = sk.getEncoded();
|
||||
System.out.println(" OK secret: " + a + " alg=" + sk.getAlgorithm() + " len="
|
||||
+ (raw == null ? 0 : raw.length));
|
||||
good = true;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
if (good) {
|
||||
ok++;
|
||||
}
|
||||
}
|
||||
assertTrue(ok > 0, "No entries could be materialized back");
|
||||
}
|
||||
|
||||
// ---- 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private static String sanitize(String id) {
|
||||
return id.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]+", "-");
|
||||
}
|
||||
|
||||
private static String encLen(byte[] der) {
|
||||
return der == null ? "0" : Integer.toString(der.length);
|
||||
}
|
||||
}
|
||||
314
app/src/test/java/zeroecho/TagTest.java
Normal file
314
app/src/test/java/zeroecho/TagTest.java
Normal file
@@ -0,0 +1,314 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.
|
||||
******************************************************************************/
|
||||
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.assertTrue;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
import org.apache.commons.cli.Options;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import zeroecho.core.storage.KeyringStore;
|
||||
import zeroecho.sdk.util.BouncyCastleActivator;
|
||||
|
||||
/**
|
||||
* CLI-level tests for the Tag subcommand (signature + digest).
|
||||
*
|
||||
* <h2>Scope</h2>
|
||||
* <ul>
|
||||
* <li>Signature produce+verify with Ed25519 via a real keyring file,</li>
|
||||
* <li>Signature verify failure appends marker text and returns rc=1,</li>
|
||||
* <li>Digest produce+verify (SHA-256) round trip,</li>
|
||||
* <li>Digest verify failure appends marker text and returns rc=1,</li>
|
||||
* <li>Digest round trip over STDIN/STDOUT.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class TagTest {
|
||||
|
||||
@TempDir
|
||||
Path tmp;
|
||||
|
||||
private PrintStream savedOut;
|
||||
private PrintStream savedErr;
|
||||
private java.io.InputStream savedIn;
|
||||
|
||||
@BeforeAll
|
||||
static void bootBouncyCastle() {
|
||||
BouncyCastleActivator.init();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void restoreStd() {
|
||||
if (savedOut != null) {
|
||||
System.setOut(savedOut);
|
||||
savedOut = null;
|
||||
}
|
||||
if (savedErr != null) {
|
||||
System.setErr(savedErr);
|
||||
savedErr = null;
|
||||
}
|
||||
if (savedIn != null) {
|
||||
System.setIn(savedIn);
|
||||
savedIn = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- boilerplate logging ----------
|
||||
private static void logBegin(Object... params) {
|
||||
String thisClass = TagTest.class.getName();
|
||||
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
|
||||
.walk(frames -> frames
|
||||
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin"))
|
||||
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
|
||||
System.out.println(method + "(" + Arrays.deepToString(params) + ")");
|
||||
}
|
||||
|
||||
private static void logEnd() {
|
||||
String thisClass = TagTest.class.getName();
|
||||
String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
|
||||
.walk(frames -> frames
|
||||
.dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd"))
|
||||
.findFirst().map(StackWalker.StackFrame::getMethodName).orElse("<?>"));
|
||||
System.out.println(method + "...ok");
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
/** Ed25519 signature round-trip using files. */
|
||||
@Test
|
||||
void signature_ed25519_roundTrip_files_ok() throws Exception {
|
||||
logBegin("signature_ed25519_roundTrip_files_ok");
|
||||
|
||||
Path ring = tmp.resolve("ring-ed25519.txt");
|
||||
KeyAliases ed = generateIntoKeyStore(ring, "Ed25519", "ed");
|
||||
// sanity
|
||||
KeyringStore ks = KeyringStore.load(ring);
|
||||
assertTrue(ks.contains(ed.pub) && ks.contains(ed.prv), "missing expected aliases");
|
||||
|
||||
byte[] pt = randomBytes(4096);
|
||||
Path plain = tmp.resolve("plain.bin");
|
||||
Path signed = tmp.resolve("signed.bin");
|
||||
Path recovered = tmp.resolve("recovered.bin");
|
||||
Files.write(plain, pt);
|
||||
|
||||
// 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");
|
||||
|
||||
// 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");
|
||||
|
||||
assertArrayEquals(pt, Files.readAllBytes(recovered), "round-trip mismatch");
|
||||
|
||||
logEnd();
|
||||
}
|
||||
|
||||
@Test
|
||||
void signature_verify_mismatch_throws_and_appends_text() throws Exception {
|
||||
logBegin("signature_verify_mismatch_throws_and_appends_text");
|
||||
|
||||
Path ring = tmp.resolve("ring-ed25519-neg.txt");
|
||||
KeyAliases ed = generateIntoKeyStore(ring, "Ed25519", "ed-neg");
|
||||
|
||||
byte[] pt = randomBytes(1024);
|
||||
Path plain = tmp.resolve("plain-neg.bin");
|
||||
Path signed = tmp.resolve("signed-neg.bin");
|
||||
Path out = tmp.resolve("out-neg.bin");
|
||||
Files.write(plain, pt);
|
||||
|
||||
// produce
|
||||
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()));
|
||||
|
||||
// corrupt last byte -> break signature
|
||||
flipLastByte(signed);
|
||||
|
||||
// verify (mismatch): expect throw + marker appended
|
||||
assertEquals(1,
|
||||
Tag.main(
|
||||
new String[] { "--type", "signature", "--mode", "verify", "--alg", "Ed25519", "--ks",
|
||||
ring.toString(), "--pub", ed.pub, "--in", signed.toString(), "--out", out.toString() },
|
||||
new Options()));
|
||||
|
||||
assertTrue(Files.notExists(out, LinkOption.NOFOLLOW_LINKS));
|
||||
|
||||
logEnd();
|
||||
}
|
||||
|
||||
/** SHA-256 digest round-trip using files. */
|
||||
@Test
|
||||
void digest_sha256_roundTrip_files_ok() throws Exception {
|
||||
logBegin("digest_sha256_roundTrip_files_ok");
|
||||
|
||||
byte[] pt = randomBytes(5000);
|
||||
Path plain = tmp.resolve("plain-d.bin");
|
||||
Path tagged = tmp.resolve("tagged-d.bin");
|
||||
Path recovered = tmp.resolve("recovered-d.bin");
|
||||
Files.write(plain, pt);
|
||||
|
||||
// produce
|
||||
assertEquals(0, Tag.main(new String[] { "--type", "digest", "--mode", "produce", "--alg", "SHA-256", "--in",
|
||||
plain.toString(), "--out", tagged.toString() }, new Options()));
|
||||
|
||||
// verify (match)
|
||||
assertEquals(0, Tag.main(new String[] { "--type", "digest", "--mode", "verify", "--alg", "SHA-256", "--in",
|
||||
tagged.toString(), "--out", recovered.toString() }, new Options()));
|
||||
|
||||
assertArrayEquals(pt, Files.readAllBytes(recovered), "digest round-trip mismatch");
|
||||
|
||||
logEnd();
|
||||
}
|
||||
|
||||
@Test
|
||||
void digest_verify_mismatch_throws_and_appends_text() throws Exception {
|
||||
logBegin("digest_verify_mismatch_throws_and_appends_text");
|
||||
|
||||
byte[] pt = randomBytes(2048);
|
||||
Path plain = tmp.resolve("plain-d-neg.bin");
|
||||
Path tagged = tmp.resolve("tagged-d-neg.bin");
|
||||
Path out = tmp.resolve("out-d-neg.bin");
|
||||
Files.write(plain, pt);
|
||||
|
||||
// produce
|
||||
assertEquals(0, Tag.main(new String[] { "--type", "digest", "--mode", "produce", "--alg", "SHA-256", "--in",
|
||||
plain.toString(), "--out", tagged.toString() }, new Options()));
|
||||
|
||||
// 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()));
|
||||
|
||||
assertTrue(Files.notExists(out, LinkOption.NOFOLLOW_LINKS));
|
||||
|
||||
logEnd();
|
||||
}
|
||||
|
||||
/** Digest round trip over STDIN/STDOUT. */
|
||||
@Test
|
||||
void digest_roundTrip_stdin_stdout_ok() throws Exception {
|
||||
logBegin("digest_roundTrip_stdin_stdout_ok");
|
||||
|
||||
byte[] pt = randomBytes(3000);
|
||||
|
||||
// produce (stdin -> stdout)
|
||||
savedIn = System.in;
|
||||
savedOut = System.out;
|
||||
ByteArrayInputStream src = new ByteArrayInputStream(pt);
|
||||
ByteArrayOutputStream producedSink = new ByteArrayOutputStream();
|
||||
System.setIn(src);
|
||||
System.setOut(new PrintStream(producedSink, true, StandardCharsets.UTF_8));
|
||||
|
||||
assertEquals(0, Tag.main(
|
||||
new String[] { "--type", "digest", "--mode", "produce", "--alg", "SHA-256", "--in", "-", "--out", "-" },
|
||||
new Options()));
|
||||
|
||||
// save produced bytes
|
||||
Path tagged = tmp.resolve("stdio-tagged.bin");
|
||||
Files.write(tagged, producedSink.toByteArray());
|
||||
|
||||
// verify (file -> stdout)
|
||||
ByteArrayOutputStream verifiedSink = new ByteArrayOutputStream();
|
||||
System.setIn(savedIn); // not used in this step
|
||||
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()));
|
||||
|
||||
assertArrayEquals(pt, verifiedSink.toByteArray(), "stdio round-trip mismatch");
|
||||
|
||||
logEnd();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/** Generates an asymmetric keypair using the real KeyStoreManagement CLI. */
|
||||
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());
|
||||
if (rc != 0) {
|
||||
throw new GeneralSecurityException("KeyStoreManagement failed with rc=" + rc + " for " + algId);
|
||||
}
|
||||
return new KeyAliases(baseAlias + ".pub", baseAlias + ".prv");
|
||||
}
|
||||
|
||||
private static void flipLastByte(Path file) throws Exception {
|
||||
byte[] all = Files.readAllBytes(file);
|
||||
if (all.length == 0) {
|
||||
throw new IllegalStateException("cannot flip byte in empty file");
|
||||
}
|
||||
all[all.length - 1] ^= 0xFF;
|
||||
Files.write(file, all);
|
||||
}
|
||||
|
||||
private static byte[] randomBytes(int n) {
|
||||
byte[] b = new byte[n];
|
||||
new Random(0xBADC0FFEL).nextBytes(b);
|
||||
return b;
|
||||
}
|
||||
|
||||
private static final class KeyAliases {
|
||||
final String pub;
|
||||
final String prv;
|
||||
|
||||
KeyAliases(String pub, String prv) {
|
||||
this.pub = pub;
|
||||
this.prv = prv;
|
||||
}
|
||||
}
|
||||
}
|
||||
74
app/src/test/java/zeroecho/ZeroEchoTest.java
Normal file
74
app/src/test/java/zeroecho/ZeroEchoTest.java
Normal file
@@ -0,0 +1,74 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2025, 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.
|
||||
******************************************************************************/
|
||||
package zeroecho;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ZeroEchoTest {
|
||||
|
||||
@Test
|
||||
void testAsymetricOptionWithoutParamsReturnsOne() {
|
||||
System.out.println("testAsymetricOptionWithoutParamsReturnsOne");
|
||||
int result = ZeroEcho.mainProcess(new String[] { "-A" });
|
||||
assertEquals(1, result, "Asymetric option without parameters should return 1 (error/help)");
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAesPswOptionWithoutParamsReturnsOne() {
|
||||
System.out.println("testAesPswOptionWithoutParamsReturnsOne");
|
||||
int result = ZeroEcho.mainProcess(new String[] { "-P" });
|
||||
assertEquals(1, result, "AES-PSW option without parameters should return 1 (error/help)");
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNoOptionReturnsOne() {
|
||||
System.out.println("testNoOptionReturnsOne");
|
||||
int result = ZeroEcho.mainProcess(new String[] {});
|
||||
assertEquals(1, result, "No options should return 1 (error/help)");
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidOptionReturnsOne() {
|
||||
System.out.println("testInvalidOptionReturnsOne");
|
||||
int result = ZeroEcho.mainProcess(new String[] { "-X" });
|
||||
assertEquals(1, result, "Invalid option should return 1 (error/help)");
|
||||
System.out.println("...ok");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user