security(lib,storage): enforce single-use encryption contexts, encrypt keyring and harden key import
This commit is contained in:
@@ -56,6 +56,7 @@ import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.context.EncryptionContext;
|
||||
import zeroecho.core.context.KemContext;
|
||||
import zeroecho.core.err.UnsupportedRoleException;
|
||||
import zeroecho.core.spi.KeyringUnlockProvider;
|
||||
import zeroecho.core.storage.KeyringStore;
|
||||
import zeroecho.sdk.Pbkdf2Limits;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
@@ -66,7 +67,7 @@ import zeroecho.sdk.guard.MultiRecipientContent;
|
||||
import zeroecho.sdk.guard.MultiRecipientDataSourceBuilder;
|
||||
import zeroecho.sdk.guard.RecipientKekSizes;
|
||||
import zeroecho.sdk.guard.UnlockMaterial;
|
||||
import zeroecho.sdk.util.RandomSupport;
|
||||
import zeroecho.core.util.RandomSupport;
|
||||
|
||||
/**
|
||||
* Guard is a unified subcommand that encrypts and decrypts using a
|
||||
@@ -111,6 +112,7 @@ import zeroecho.sdk.util.RandomSupport;
|
||||
* --alg chacha-aead
|
||||
* }</pre>
|
||||
*/
|
||||
@SuppressWarnings("PMD.CyclomaticComplexity")
|
||||
public final class Guard {
|
||||
|
||||
private Guard() {
|
||||
@@ -128,7 +130,26 @@ public final class Guard {
|
||||
* @throws IOException on I/O errors
|
||||
* @throws GeneralSecurityException on cryptographic setup or keyring errors
|
||||
*/
|
||||
public static int main(final String[] args, final Options options) // NOPMD
|
||||
public static int main(final String[] args, final Options options)
|
||||
throws ParseException, IOException, GeneralSecurityException {
|
||||
return main(args, options, KeyringUnlocks.console());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes Guard with an explicit keyring unlock source.
|
||||
*
|
||||
* @param args command arguments
|
||||
* @param options dispatcher options
|
||||
* @param keyringUnlockProvider destroyable-password provider
|
||||
* @return process exit code
|
||||
* @throws ParseException if parsing fails
|
||||
* @throws IOException if I/O fails
|
||||
* @throws GeneralSecurityException if cryptographic processing fails
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.NcssCount", "PMD.CognitiveComplexity",
|
||||
"PMD.CyclomaticComplexity", "PMD.NPathComplexity" })
|
||||
public static int main(final String[] args, final Options options,
|
||||
KeyringUnlockProvider keyringUnlockProvider)
|
||||
throws ParseException, IOException, GeneralSecurityException {
|
||||
// ---- operation selection
|
||||
final Option OPT_ENCRYPT = Option.builder("e").longOpt("encrypt").hasArg().argName("in-file")
|
||||
@@ -356,40 +377,43 @@ public final class Guard {
|
||||
final int kekLen = RecipientKekSizes.requireSupported(
|
||||
Integer.parseInt(cmd.getOptionValue(OPT_PSW_KEK, "32")));
|
||||
|
||||
final KeyringStore ks = loadKeyringIfPresent(session, cmd, OPT_KEYRING);
|
||||
for (String alias : cmd.getOptionValues(OPT_TO_ALIAS) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_TO_ALIAS)) {
|
||||
addRecipientFromAlias(session, env, ks, alias, kekLen, saltLen, false);
|
||||
}
|
||||
for (String psw : cmd.getOptionValues(OPT_TO_PSW) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_TO_PSW)) {
|
||||
char[] passwordChars = psw.toCharArray();
|
||||
try {
|
||||
env.addPasswordRecipient(passwordChars, iter, saltLen, kekLen);
|
||||
} finally {
|
||||
Arrays.fill(passwordChars, '\0');
|
||||
KeyringStore ks = loadKeyringIfPresent(cmd, OPT_KEYRING,
|
||||
keyringUnlockProvider);
|
||||
try (ks) {
|
||||
for (String alias : cmd.getOptionValues(OPT_TO_ALIAS) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_TO_ALIAS)) {
|
||||
addRecipientFromAlias(session, env, ks, alias, kekLen, saltLen, false);
|
||||
}
|
||||
}
|
||||
for (String alias : cmd.getOptionValues(OPT_DECOY_ALIAS) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_DECOY_ALIAS)) {
|
||||
addRecipientFromAlias(session, env, ks, alias, kekLen, saltLen, true);
|
||||
}
|
||||
for (String psw : cmd.getOptionValues(OPT_DECOY_PSW) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_DECOY_PSW)) {
|
||||
char[] passwordChars = psw.toCharArray();
|
||||
try {
|
||||
env.addPasswordRecipientDecoy(passwordChars, iter, saltLen, kekLen);
|
||||
} finally {
|
||||
Arrays.fill(passwordChars, '\0');
|
||||
for (String psw : cmd.getOptionValues(OPT_TO_PSW) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_TO_PSW)) {
|
||||
char[] passwordChars = psw.toCharArray();
|
||||
try {
|
||||
env.addPasswordRecipient(passwordChars, iter, saltLen, kekLen);
|
||||
} finally {
|
||||
Arrays.fill(passwordChars, '\0');
|
||||
}
|
||||
}
|
||||
}
|
||||
final int rndCount = Integer.parseInt(cmd.getOptionValue(OPT_DECOY_PSW_RAND, "0"));
|
||||
for (int i = 0; i < rndCount; i++) {
|
||||
char[] passwordChars = randomPassword();
|
||||
try {
|
||||
env.addPasswordRecipientDecoy(passwordChars, iter, saltLen, kekLen);
|
||||
} finally {
|
||||
Arrays.fill(passwordChars, '\0');
|
||||
for (String alias : cmd.getOptionValues(OPT_DECOY_ALIAS) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_DECOY_ALIAS)) {
|
||||
addRecipientFromAlias(session, env, ks, alias, kekLen, saltLen, true);
|
||||
}
|
||||
for (String psw : cmd.getOptionValues(OPT_DECOY_PSW) == null ? new String[0]
|
||||
: cmd.getOptionValues(OPT_DECOY_PSW)) {
|
||||
char[] passwordChars = psw.toCharArray();
|
||||
try {
|
||||
env.addPasswordRecipientDecoy(passwordChars, iter, saltLen, kekLen);
|
||||
} finally {
|
||||
Arrays.fill(passwordChars, '\0');
|
||||
}
|
||||
}
|
||||
final int rndCount = Integer.parseInt(cmd.getOptionValue(OPT_DECOY_PSW_RAND, "0"));
|
||||
for (int i = 0; i < rndCount; i++) {
|
||||
char[] passwordChars = randomPassword();
|
||||
try {
|
||||
env.addPasswordRecipientDecoy(passwordChars, iter, saltLen, kekLen);
|
||||
} finally {
|
||||
Arrays.fill(passwordChars, '\0');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -400,9 +424,11 @@ public final class Guard {
|
||||
throw new ParseException("Specify exactly one of --priv-alias or --password for decryption");
|
||||
}
|
||||
if (privAlias != null) {
|
||||
final KeyringStore ks = requireKeyring(session, cmd, OPT_KEYRING);
|
||||
final KeyringStore.PrivateWithId pr = ks.getPrivateWithId(privAlias);
|
||||
borrowedUnlockMaterial = new UnlockMaterial.Private(pr.key());
|
||||
try (KeyringStore ks = requireKeyring(cmd, OPT_KEYRING,
|
||||
keyringUnlockProvider)) {
|
||||
final KeyringStore.PrivateWithId pr = ks.getPrivateWithId(privAlias);
|
||||
borrowedUnlockMaterial = new UnlockMaterial.Private(pr.key());
|
||||
}
|
||||
} else {
|
||||
char[] passwordChars = password.toCharArray();
|
||||
try {
|
||||
@@ -575,19 +601,21 @@ public final class Guard {
|
||||
return out;
|
||||
}
|
||||
|
||||
private static KeyringStore loadKeyringIfPresent(ZeroEchoSession session, CommandLine cmd, Option optKs)
|
||||
throws IOException {
|
||||
private static KeyringStore loadKeyringIfPresent(CommandLine cmd, Option optKs,
|
||||
KeyringUnlockProvider unlockProvider)
|
||||
throws IOException, GeneralSecurityException {
|
||||
if (!cmd.hasOption(optKs)) {
|
||||
return new KeyringStore(session);
|
||||
return null;
|
||||
}
|
||||
return KeyringStore.load(session, Paths.get(cmd.getOptionValue(optKs)));
|
||||
return KeyringUnlocks.open(Paths.get(cmd.getOptionValue(optKs)), unlockProvider);
|
||||
}
|
||||
|
||||
private static KeyringStore requireKeyring(ZeroEchoSession session, CommandLine cmd, Option optKs)
|
||||
throws IOException, ParseException {
|
||||
private static KeyringStore requireKeyring(CommandLine cmd, Option optKs,
|
||||
KeyringUnlockProvider unlockProvider)
|
||||
throws IOException, ParseException, GeneralSecurityException {
|
||||
if (!cmd.hasOption(optKs)) {
|
||||
throw new ParseException("--keyring <file> is required when aliases are used");
|
||||
}
|
||||
return KeyringStore.load(session, Paths.get(cmd.getOptionValue(optKs)));
|
||||
return KeyringUnlocks.open(Paths.get(cmd.getOptionValue(optKs)), unlockProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ import org.apache.commons.cli.ParseException;
|
||||
import zeroecho.core.AlgorithmFamily;
|
||||
import zeroecho.core.CatalogSelector;
|
||||
import zeroecho.core.KeyUsage;
|
||||
import zeroecho.core.spi.KeyringUnlockProvider;
|
||||
import zeroecho.core.storage.KeyringStore;
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder;
|
||||
@@ -203,7 +204,37 @@ public final class Kem { // NOPMD
|
||||
// no instances
|
||||
}
|
||||
|
||||
public static int main(String[] args, Options opts) throws ParseException, IOException, GeneralSecurityException { // NOPMD
|
||||
/**
|
||||
* Executes the KEM command using interactive keyring unlocking.
|
||||
*
|
||||
* @param args command arguments
|
||||
* @param opts command options
|
||||
* @return process exit code
|
||||
* @throws ParseException if arguments are invalid
|
||||
* @throws IOException if I/O fails
|
||||
* @throws GeneralSecurityException if cryptographic processing fails
|
||||
*/
|
||||
public static int main(String[] args, Options opts)
|
||||
throws ParseException, IOException, GeneralSecurityException {
|
||||
return main(args, opts, KeyringUnlocks.console());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the KEM command with an explicit keyring unlock source.
|
||||
*
|
||||
* @param args command arguments
|
||||
* @param opts command options
|
||||
* @param unlockProvider keyring password provider
|
||||
* @return process exit code
|
||||
* @throws ParseException if arguments are invalid
|
||||
* @throws IOException if I/O fails
|
||||
* @throws GeneralSecurityException if cryptographic processing fails
|
||||
*/
|
||||
@SuppressWarnings({ "PMD.NcssCount", "PMD.CyclomaticComplexity",
|
||||
"PMD.NPathComplexity" })
|
||||
public static int main(String[] args, Options opts,
|
||||
KeyringUnlockProvider unlockProvider)
|
||||
throws ParseException, IOException, GeneralSecurityException {
|
||||
ZeroEchoSession session = new ZeroEchoSession();
|
||||
defineOptions(opts);
|
||||
CommandLineParser parser = new DefaultParser();
|
||||
@@ -242,7 +273,7 @@ public final class Kem { // NOPMD
|
||||
|
||||
final String kemId = cmd.getOptionValue(OPT_KEM.getLongOpt());
|
||||
final Path keyringPath = Path.of(cmd.getOptionValue(OPT_KEYRING.getLongOpt()));
|
||||
final KeyringStore keyring = KeyringStore.load(session, keyringPath);
|
||||
try (KeyringStore keyring = KeyringUnlocks.open(keyringPath, unlockProvider)) {
|
||||
|
||||
// Configure KEM envelope
|
||||
KemDataContentBuilder kem = KemDataContentBuilder.builder(session).kem(kemId);
|
||||
@@ -343,7 +374,8 @@ public final class Kem { // NOPMD
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,19 +33,13 @@
|
||||
******************************************************************************/
|
||||
package zeroecho;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
@@ -65,21 +59,19 @@ import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.KeyOperation;
|
||||
import zeroecho.core.KeyOperationInfo;
|
||||
import zeroecho.core.spec.AlgorithmKeySpec;
|
||||
import zeroecho.core.spi.KeyringUnlockProvider;
|
||||
import zeroecho.core.storage.KeyringStore;
|
||||
import zeroecho.sdk.ZeroEchoSession;
|
||||
|
||||
/**
|
||||
* Command-line utility for managing key material in a text-based keyring store.
|
||||
* Command-line utility for managing an encrypted software keyring.
|
||||
*
|
||||
* <h2>Overview</h2> The {@code KeyStoreManagement} subcommand provides
|
||||
* lifecycle operations on key material stored in
|
||||
* {@link zeroecho.core.storage.KeyringStore}. It supports listing algorithms
|
||||
* and aliases, generating key pairs or symmetric keys, exporting/importing
|
||||
* versioned text snippets, and overwriting existing entries.
|
||||
*
|
||||
* The keystore is a plain-text file with aliases mapped to keys. Import/export
|
||||
* uses line-oriented snippets with format-version headers, suitable for
|
||||
* exchanging public keys or migrating key material between systems.
|
||||
* and aliases and generating key pairs or symmetric keys. Passwords are
|
||||
* supplied through a destroyable unlock provider and are never accepted as
|
||||
* command-line text.
|
||||
*
|
||||
* <h2>Usage</h2> Invoked as: <pre>{@code
|
||||
* ZeroEcho -K [options]
|
||||
@@ -92,9 +84,6 @@ import zeroecho.sdk.ZeroEchoSession;
|
||||
* <li>{@code --list-aliases} - list aliases present in the keystore.</li>
|
||||
* <li>{@code --generate} - generate a new key pair or secret and store under
|
||||
* the given alias.</li>
|
||||
* <li>{@code --export} - export one or more aliases as a versioned
|
||||
* snippet.</li>
|
||||
* <li>{@code --import} - import a versioned snippet into the keystore.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>General options</h2>
|
||||
@@ -116,38 +105,18 @@ import zeroecho.sdk.ZeroEchoSession;
|
||||
* .prv).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Export options</h2>
|
||||
* <ul>
|
||||
* <li>{@code --aliases a,b,c} - comma-separated list of aliases to export
|
||||
* (default: all).</li>
|
||||
* <li>{@code --out <file|-} - output file path (default: "-" for stdout).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Import options</h2>
|
||||
* <ul>
|
||||
* <li>{@code --in <file|-} - input file path (default: "-" for stdin).</li>
|
||||
* <li>{@code --overwrite} - allow replacing existing aliases when
|
||||
* importing.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Examples</h2> <pre>{@code
|
||||
* # List available algorithms
|
||||
* ZeroEcho -K --list-algorithms
|
||||
*
|
||||
* # Generate a new RSA key pair and store as alice.pub / alice.prv
|
||||
* ZeroEcho -K --generate --alg RSA --alias alice --keystore keys.txt
|
||||
* ZeroEcho -K --generate --alg RSA --alias alice --keystore keys.zekr
|
||||
*
|
||||
* # Generate a new AES secret key and store as "backup-key"
|
||||
* ZeroEcho -K --generate --alg AES --alias backup-key --kind sym --keystore keys.txt
|
||||
* ZeroEcho -K --generate --alg AES --alias backup-key --kind sym --keystore keys.zekr
|
||||
*
|
||||
* # List aliases in the keystore
|
||||
* ZeroEcho -K --list-aliases --keystore keys.txt
|
||||
*
|
||||
* # Export selected aliases to a file
|
||||
* ZeroEcho -K --export --aliases alice.pub,alice.prv --out alice-keys.txt --keystore keys.txt
|
||||
*
|
||||
* # Import aliases from stdin, overwriting if necessary
|
||||
* ZeroEcho -K --import --in - --overwrite --keystore keys.txt < alice-keys.txt
|
||||
* ZeroEcho -K --list-aliases --keystore keys.zekr
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Exit codes</h2>
|
||||
@@ -159,9 +128,7 @@ import zeroecho.sdk.ZeroEchoSession;
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
public final class KeyStoreManagement { // NOPMD
|
||||
|
||||
private final static String STD_IN_OUT = "-";
|
||||
public final class KeyStoreManagement {
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Option constants (centralized for maintainability)
|
||||
@@ -197,21 +164,6 @@ public final class KeyStoreManagement { // NOPMD
|
||||
private static final Option OVERWRITE_OPTION = Option.builder().longOpt("overwrite")
|
||||
.desc("Overwrite existing aliases on conflict").get();
|
||||
|
||||
private static final Option EXPORT_OPTION = Option.builder().longOpt("export")
|
||||
.desc("Export selected aliases as a versioned text snippet").get();
|
||||
|
||||
private static final Option IMPORT_OPTION = Option.builder().longOpt("import")
|
||||
.desc("Import a versioned text snippet into the keyring").get();
|
||||
|
||||
private static final Option ALIASES_OPTION = Option.builder().longOpt("aliases").hasArg().argName("a,b,c")
|
||||
.desc("Comma-separated aliases to export; empty means all").get();
|
||||
|
||||
private static final Option OUTFILE_OPTION = Option.builder().longOpt("out").hasArg().argName("file|-")
|
||||
.desc("Output file for export (default '-' for stdout)").get();
|
||||
|
||||
private static final Option INFILE_OPTION = Option.builder().longOpt("in").hasArg().argName("file|-")
|
||||
.desc("Input file for import (default '-' for stdin)").get();
|
||||
|
||||
/** Prevents instantiation. */
|
||||
private KeyStoreManagement() {
|
||||
}
|
||||
@@ -232,7 +184,25 @@ public final class KeyStoreManagement { // NOPMD
|
||||
* @throws ParseException if the parser fails
|
||||
* @throws IOException if I/O fails
|
||||
*/
|
||||
public static int main(final String[] args, final Options dispatcherOptions) throws ParseException, IOException {
|
||||
public static int main(final String[] args, final Options dispatcherOptions)
|
||||
throws ParseException, IOException, GeneralSecurityException {
|
||||
return main(args, dispatcherOptions, KeyringUnlocks.console());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the command with an explicit unlock source.
|
||||
*
|
||||
* @param args arguments passed by the application dispatcher
|
||||
* @param dispatcherOptions dispatcher options
|
||||
* @param unlockProvider explicit destroyable-password provider
|
||||
* @return process exit code
|
||||
* @throws ParseException if parsing fails
|
||||
* @throws IOException if keyring I/O fails
|
||||
* @throws GeneralSecurityException if cryptographic processing fails
|
||||
*/
|
||||
public static int main(final String[] args, final Options dispatcherOptions,
|
||||
KeyringUnlockProvider unlockProvider)
|
||||
throws ParseException, IOException, GeneralSecurityException {
|
||||
ZeroEchoSession session = new ZeroEchoSession();
|
||||
defineOptions(dispatcherOptions);
|
||||
CommandLineParser parser = new DefaultParser();
|
||||
@@ -248,26 +218,17 @@ public final class KeyStoreManagement { // NOPMD
|
||||
}
|
||||
|
||||
Path keyringPath = Path.of(cmd.getOptionValue(KEYSTORE_OPTION.getLongOpt()));
|
||||
KeyringStore store = Files.exists(keyringPath) ? KeyringStore.load(session, keyringPath)
|
||||
: new KeyringStore(session);
|
||||
|
||||
if (cmd.hasOption(LIST_ALIASES_OPTION.getLongOpt())) {
|
||||
listAliases(store);
|
||||
return 0;
|
||||
}
|
||||
if (cmd.hasOption(GENERATE_OPTION.getLongOpt())) {
|
||||
doGenerate(session, store, cmd);
|
||||
store.save(keyringPath);
|
||||
return 0;
|
||||
}
|
||||
if (cmd.hasOption(EXPORT_OPTION.getLongOpt())) {
|
||||
doExportSnippet(store, cmd);
|
||||
return 0;
|
||||
}
|
||||
if (cmd.hasOption(IMPORT_OPTION.getLongOpt())) {
|
||||
doImportSnippet(store, cmd);
|
||||
store.save(keyringPath);
|
||||
return 0;
|
||||
try (KeyringStore store = Files.exists(keyringPath)
|
||||
? KeyringUnlocks.open(keyringPath, unlockProvider)
|
||||
: KeyringUnlocks.create(keyringPath, unlockProvider)) {
|
||||
if (cmd.hasOption(LIST_ALIASES_OPTION.getLongOpt())) {
|
||||
listAliases(store);
|
||||
return 0;
|
||||
}
|
||||
if (cmd.hasOption(GENERATE_OPTION.getLongOpt())) {
|
||||
doGenerate(session, store, cmd);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("No operation selected");
|
||||
@@ -287,8 +248,6 @@ public final class KeyStoreManagement { // NOPMD
|
||||
actions.addOption(LIST_ALGORITHMS_OPTION);
|
||||
actions.addOption(LIST_ALIASES_OPTION);
|
||||
actions.addOption(GENERATE_OPTION);
|
||||
actions.addOption(EXPORT_OPTION);
|
||||
actions.addOption(IMPORT_OPTION);
|
||||
options.addOptionGroup(actions);
|
||||
|
||||
options.addOption(ALG_OPTION);
|
||||
@@ -298,9 +257,6 @@ public final class KeyStoreManagement { // NOPMD
|
||||
options.addOption(PRV_SUFFIX_OPTION);
|
||||
options.addOption(OVERWRITE_OPTION);
|
||||
|
||||
options.addOption(ALIASES_OPTION);
|
||||
options.addOption(OUTFILE_OPTION);
|
||||
options.addOption(INFILE_OPTION);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -355,7 +311,7 @@ public final class KeyStoreManagement { // NOPMD
|
||||
* @param cmd parsed command line
|
||||
*/
|
||||
public static void doGenerate(final ZeroEchoSession session, final KeyringStore store,
|
||||
final CommandLine cmd) {
|
||||
final CommandLine cmd) throws IOException, GeneralSecurityException {
|
||||
String algId = required(cmd, ALG_OPTION, "--alg is required for --generate");
|
||||
String aliasBase = required(cmd, ALIAS_OPTION, "--alias is required for --generate");
|
||||
String kind = cmd.getOptionValue(KIND_OPTION.getLongOpt());
|
||||
@@ -393,30 +349,21 @@ public final class KeyStoreManagement { // NOPMD
|
||||
}
|
||||
|
||||
private static void generateAsymmetric(ZeroEchoSession session, KeyringStore store, CryptoAlgorithm algorithm,
|
||||
String algorithmId, String aliasBase, String publicSuffix, String privateSuffix, boolean overwrite) {
|
||||
String algorithmId, String aliasBase, String publicSuffix, String privateSuffix, boolean overwrite)
|
||||
throws IOException, GeneralSecurityException {
|
||||
GeneratedKeyPair generated = firstGeneratedKeyPair(session, algorithm, algorithmId);
|
||||
Class<?> publicImport = findImportSpecClass(algorithm, KeyOperation.ASYMMETRIC_PUBLIC_IMPORT, true);
|
||||
Class<?> privateImport = findImportSpecClass(algorithm, KeyOperation.ASYMMETRIC_PRIVATE_IMPORT, false);
|
||||
if (publicImport == null && privateImport == null) {
|
||||
throw new IllegalStateException("No import spec class found for " + algorithmId + " (asymmetric)");
|
||||
}
|
||||
|
||||
KeyPair pair = generated.pair();
|
||||
byte[] publicEncoding = pair.getPublic() == null ? null : pair.getPublic().getEncoded();
|
||||
byte[] privateEncoding = pair.getPrivate() == null ? null : pair.getPrivate().getEncoded();
|
||||
AlgorithmKeySpec publicSpec = publicImport == null ? null
|
||||
: makeImportSpec(publicImport, publicEncoding, algorithmId, generated.info().defaultSpec());
|
||||
AlgorithmKeySpec privateSpec = privateImport == null ? null
|
||||
: makeImportSpec(privateImport, privateEncoding, algorithmId, generated.info().defaultSpec());
|
||||
requireImportSpec(publicImport, publicSpec, "public", algorithmId);
|
||||
requireImportSpec(privateImport, privateSpec, "private", algorithmId);
|
||||
if (pair.getPublic() == null || pair.getPrivate() == null) {
|
||||
throw new IllegalStateException("Generated key pair is incomplete");
|
||||
}
|
||||
|
||||
String publicAlias = aliasBase + publicSuffix;
|
||||
String privateAlias = aliasBase + privateSuffix;
|
||||
ensureWritable(store, publicAlias, overwrite);
|
||||
ensureWritable(store, privateAlias, overwrite);
|
||||
store.putPublic(publicAlias, algorithmId, publicSpec);
|
||||
store.putPrivate(privateAlias, algorithmId, privateSpec);
|
||||
store.putPublic(publicAlias, algorithmId, pair.getPublic());
|
||||
store.putPrivate(privateAlias, algorithmId, pair.getPrivate());
|
||||
|
||||
PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD
|
||||
out.printf("Generated %s -> %s, %s%n", algorithmId, publicAlias, privateAlias);
|
||||
@@ -443,39 +390,12 @@ public final class KeyStoreManagement { // NOPMD
|
||||
throw new IllegalStateException("No asymmetric builder with default spec worked for " + algorithmId);
|
||||
}
|
||||
|
||||
private static Class<?> findImportSpecClass(CryptoAlgorithm algorithm, KeyOperation operation,
|
||||
boolean publicImport) {
|
||||
for (KeyOperationInfo info : algorithm.keyOperations()) {
|
||||
boolean matchingName = publicImport ? looksLikeImportSpecForPublic(info.specType())
|
||||
: looksLikeImportSpecForPrivate(info.specType());
|
||||
if (info.operation() == operation && matchingName) {
|
||||
return info.specType();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void requireImportSpec(Class<?> specType, AlgorithmKeySpec spec, String kind, String algorithmId) {
|
||||
if (specType != null && spec == null) {
|
||||
throw new IllegalStateException("Cannot construct " + kind + " import spec for " + algorithmId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void generateSymmetric(ZeroEchoSession session, KeyringStore store, CryptoAlgorithm algorithm,
|
||||
String algorithmId, String alias, boolean overwrite) {
|
||||
String algorithmId, String alias, boolean overwrite)
|
||||
throws IOException, GeneralSecurityException {
|
||||
GeneratedSecret generated = firstGeneratedSecret(session, algorithm, algorithmId);
|
||||
Class<?> importType = findSymmetricImportSpecClass(algorithm);
|
||||
if (importType == null) {
|
||||
throw new IllegalStateException("No symmetric import spec class for " + algorithmId);
|
||||
}
|
||||
|
||||
byte[] encoding = generated.key().getEncoded();
|
||||
AlgorithmKeySpec spec = makeImportSpec(importType, encoding, algorithmId, generated.info().defaultSpec());
|
||||
if (spec == null) {
|
||||
throw new IllegalStateException("Cannot construct symmetric import spec for " + algorithmId);
|
||||
}
|
||||
ensureWritable(store, alias, overwrite);
|
||||
store.putSecret(alias, algorithmId, spec);
|
||||
store.putSecret(alias, algorithmId, generated.key());
|
||||
|
||||
PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD
|
||||
out.printf("Generated %s -> %s%n", algorithmId, alias);
|
||||
@@ -515,163 +435,6 @@ public final class KeyStoreManagement { // NOPMD
|
||||
NONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a versioned, line-oriented snippet to stdout or a file.
|
||||
*
|
||||
* @param store source keyring store
|
||||
* @param cmd parsed command line
|
||||
* @throws IOException if writing fails
|
||||
*/
|
||||
public static void doExportSnippet(final KeyringStore store, final CommandLine cmd) throws IOException {
|
||||
List<String> aliases = parseCsv(cmd.getOptionValue(ALIASES_OPTION.getLongOpt(), ""));
|
||||
if (aliases.isEmpty()) {
|
||||
aliases = store.aliases();
|
||||
}
|
||||
|
||||
String text = store.exportText(aliases);
|
||||
String outFile = cmd.getOptionValue(OUTFILE_OPTION.getLongOpt(), STD_IN_OUT);
|
||||
|
||||
if (STD_IN_OUT.equals(outFile)) {
|
||||
try (PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8)) {
|
||||
out.print(text);
|
||||
if (!text.endsWith("\n")) {
|
||||
out.println();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Files.writeString(Path.of(outFile), text, StandardCharsets.UTF_8);
|
||||
System.out.printf("Wrote snippet: %s%n", outFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a versioned, line-oriented snippet from stdin or a file.
|
||||
*
|
||||
* @param store destination keyring store
|
||||
* @param cmd parsed command line
|
||||
* @throws IOException if reading fails or the snippet is invalid
|
||||
*/
|
||||
public static void doImportSnippet(final KeyringStore store, final CommandLine cmd) throws IOException {
|
||||
boolean overwrite = cmd.hasOption(OVERWRITE_OPTION.getLongOpt());
|
||||
String inFile = cmd.getOptionValue(INFILE_OPTION.getLongOpt(), STD_IN_OUT);
|
||||
|
||||
String text;
|
||||
if (STD_IN_OUT.equals(inFile)) {
|
||||
StringBuilder sb = new StringBuilder(1024);
|
||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
|
||||
String line = br.readLine();
|
||||
while (line != null) {
|
||||
sb.append(line).append('\n');
|
||||
line = br.readLine();
|
||||
}
|
||||
}
|
||||
text = sb.toString();
|
||||
} else {
|
||||
text = Files.readString(Path.of(inFile), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
store.importText(text, overwrite);
|
||||
PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD
|
||||
out.println("Imported snippet.");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Helpers - EXACTLY the dynamic-test strategy
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
private static boolean looksLikeImportSpecForPublic(Class<?> specType) {
|
||||
String n = specType.getSimpleName();
|
||||
return n.contains("Public") || n.endsWith("PublicKeySpec");
|
||||
}
|
||||
|
||||
private static boolean looksLikeImportSpecForPrivate(Class<?> specType) {
|
||||
String n = specType.getSimpleName();
|
||||
return n.contains("Private") || n.endsWith("PrivateKeySpec");
|
||||
}
|
||||
|
||||
private static Class<?> findSymmetricImportSpecClass(CryptoAlgorithm alg) {
|
||||
for (KeyOperationInfo x : alg.keyOperations()) {
|
||||
if (x.operation() == KeyOperation.SYMMETRIC_IMPORT) {
|
||||
return x.specType();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an AlgorithmKeySpec from encoded bytes using conventional
|
||||
* factories/ctors. Mirrors the makeImportSpec approach used in the dynamic
|
||||
* test.
|
||||
*
|
||||
* @param specType target spec class
|
||||
* @param material encoded bytes (SPKI, PKCS#8, or RAW)
|
||||
* @param algId algorithm id (used for variant name heuristics)
|
||||
* @param defaultSpec the default spec used by the builder (for optional variant
|
||||
* hints)
|
||||
* @return constructed AlgorithmKeySpec or null if none matched
|
||||
*/
|
||||
private static AlgorithmKeySpec makeImportSpec(Class<?> specType, byte[] material, String algId,
|
||||
Object defaultSpec) {
|
||||
if (material == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Method m = specType.getMethod("fromRaw", byte[].class);
|
||||
Object spec = m.invoke(null, material);
|
||||
return (AlgorithmKeySpec) spec;
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
} catch (Throwable t) { // NOPMD
|
||||
}
|
||||
try {
|
||||
Method m = specType.getMethod("fromRaw", String.class, byte[].class);
|
||||
String name = deriveVariantNameForImport(algId, defaultSpec);
|
||||
Object spec = m.invoke(null, name, material);
|
||||
return (AlgorithmKeySpec) spec;
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
} catch (Throwable t) { // NOPMD
|
||||
}
|
||||
try {
|
||||
Method m = specType.getMethod("of", byte[].class);
|
||||
Object spec = m.invoke(null, material);
|
||||
return (AlgorithmKeySpec) spec;
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
} catch (Throwable t) { // NOPMD
|
||||
}
|
||||
try {
|
||||
Constructor<?> c = specType.getConstructor(byte[].class);
|
||||
Object spec = c.newInstance(material);
|
||||
return (AlgorithmKeySpec) spec;
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
} catch (Throwable t) { // NOPMD
|
||||
}
|
||||
try {
|
||||
Constructor<?> c = specType.getConstructor(String.class);
|
||||
String b64 = Base64.getEncoder().encodeToString(material);
|
||||
Object spec = c.newInstance(b64);
|
||||
return (AlgorithmKeySpec) spec;
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
} catch (Throwable t) { // NOPMD
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String deriveVariantNameForImport(String algId, Object defaultSpec) {
|
||||
if (defaultSpec != null) {
|
||||
try {
|
||||
Method m = defaultSpec.getClass().getMethod("macName");
|
||||
Object v = m.invoke(defaultSpec);
|
||||
if (v instanceof String) {
|
||||
return (String) v;
|
||||
}
|
||||
} catch (Throwable ignored) { // NOPMD
|
||||
}
|
||||
}
|
||||
if ("HMAC".equalsIgnoreCase(algId)) { // NOPMD
|
||||
return "HmacSHA256";
|
||||
}
|
||||
return algId;
|
||||
}
|
||||
|
||||
private static String required(CommandLine cmd, Option opt, String message) {
|
||||
if (!cmd.hasOption(opt.getLongOpt())) {
|
||||
throw new IllegalArgumentException(message);
|
||||
@@ -679,20 +442,6 @@ public final class KeyStoreManagement { // NOPMD
|
||||
return cmd.getOptionValue(opt.getLongOpt());
|
||||
}
|
||||
|
||||
private static List<String> parseCsv(String csv) {
|
||||
List<String> list = new ArrayList<>();
|
||||
if (csv == null || csv.isBlank()) {
|
||||
return list;
|
||||
}
|
||||
String[] parts = csv.split("\\s*,\\s*");
|
||||
for (String part : parts) {
|
||||
if (!part.isBlank()) {
|
||||
list.add(part);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that an alias can be written under the current collision policy.
|
||||
*
|
||||
|
||||
64
app/src/main/java/zeroecho/KeyringUnlocks.java
Normal file
64
app/src/main/java/zeroecho/KeyringUnlocks.java
Normal file
@@ -0,0 +1,64 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
******************************************************************************/
|
||||
package zeroecho;
|
||||
|
||||
import java.io.Console;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import zeroecho.core.spi.KeyringUnlockProvider;
|
||||
import zeroecho.core.storage.KeyringPassword;
|
||||
import zeroecho.core.storage.KeyringStore;
|
||||
|
||||
/**
|
||||
* Application boundary for acquiring and promptly destroying keyring passwords.
|
||||
*/
|
||||
final class KeyringUnlocks {
|
||||
private KeyringUnlocks() {
|
||||
}
|
||||
|
||||
/* default */ static KeyringUnlockProvider console() {
|
||||
return () -> {
|
||||
Console console = System.console();
|
||||
if (console == null) {
|
||||
throw new IOException("Interactive keyring password input is unavailable");
|
||||
}
|
||||
char[] input = console.readPassword("Keyring password: ");
|
||||
if (input == null) {
|
||||
throw new IOException("Keyring password input was cancelled");
|
||||
}
|
||||
try {
|
||||
return new KeyringPassword(input);
|
||||
} finally {
|
||||
Arrays.fill(input, '\0');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* default */ static KeyringStore open(Path path, KeyringUnlockProvider provider)
|
||||
throws IOException, GeneralSecurityException {
|
||||
try (KeyringPassword password = acquire(provider)) {
|
||||
return KeyringStore.open(path, password);
|
||||
}
|
||||
}
|
||||
|
||||
/* default */ static KeyringStore create(Path path, KeyringUnlockProvider provider)
|
||||
throws IOException, GeneralSecurityException {
|
||||
try (KeyringPassword password = acquire(provider)) {
|
||||
return KeyringStore.create(path, password);
|
||||
}
|
||||
}
|
||||
|
||||
private static KeyringPassword acquire(KeyringUnlockProvider provider)
|
||||
throws IOException {
|
||||
KeyringPassword password = provider.acquire();
|
||||
if (password == null) {
|
||||
throw new IOException("Keyring unlock provider returned no password");
|
||||
}
|
||||
return password;
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ import zeroecho.core.alg.digest.DigestSpec;
|
||||
import zeroecho.core.err.VerificationException;
|
||||
import zeroecho.core.spec.ContextSpec;
|
||||
import zeroecho.core.spec.VoidSpec;
|
||||
import zeroecho.core.spi.KeyringUnlockProvider;
|
||||
import zeroecho.core.storage.KeyringStore;
|
||||
import zeroecho.core.tag.TagEngineBuilder;
|
||||
import zeroecho.sdk.builders.TagTrailerDataContentBuilder;
|
||||
@@ -157,7 +158,25 @@ public final class Tag { // NOPMD
|
||||
* @throws GeneralSecurityException if a cryptographic error occurs during
|
||||
* signature or digest processing
|
||||
*/
|
||||
public static int main(String[] args, Options root) throws ParseException, IOException, GeneralSecurityException {
|
||||
public static int main(String[] args, Options root)
|
||||
throws ParseException, IOException, GeneralSecurityException {
|
||||
return main(args, root, KeyringUnlocks.console());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the command with an explicit keyring unlock source.
|
||||
*
|
||||
* @param args command arguments
|
||||
* @param root root options
|
||||
* @param unlockProvider keyring password provider
|
||||
* @return process exit code
|
||||
* @throws ParseException if parsing fails
|
||||
* @throws IOException if I/O fails
|
||||
* @throws GeneralSecurityException if cryptographic processing fails
|
||||
*/
|
||||
public static int main(String[] args, Options root,
|
||||
KeyringUnlockProvider unlockProvider)
|
||||
throws ParseException, IOException, GeneralSecurityException {
|
||||
ZeroEchoSession session = new ZeroEchoSession();
|
||||
Options opts = root;
|
||||
opts.addOption(TYPE_OPT);
|
||||
@@ -198,19 +217,19 @@ public final class Tag { // NOPMD
|
||||
|
||||
if (TYPE_SIGNATURE.equals(type)) {
|
||||
String ksPath = require(cli, KS_OPT, "--ks <file> is required for --type signature");
|
||||
KeyringStore keyring = KeyringStore.load(session, Path.of(ksPath));
|
||||
ContextSpec spec = VoidSpec.INSTANCE;
|
||||
|
||||
if (produce) {
|
||||
String privAlias = require(cli, PRIV_OPT, "signature produce requires --priv <alias>");
|
||||
PrivateKey priv = keyring.getPrivate(privAlias);
|
||||
tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(session, alg, priv, spec))
|
||||
.build(true);
|
||||
} else {
|
||||
String pubAlias = require(cli, PUB_OPT, "signature verify requires --pub <alias>");
|
||||
PublicKey pub = keyring.getPublic(pubAlias);
|
||||
tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(session, alg, pub, spec))
|
||||
.build(false);
|
||||
try (KeyringStore keyring = KeyringUnlocks.open(Path.of(ksPath), unlockProvider)) {
|
||||
ContextSpec spec = VoidSpec.INSTANCE;
|
||||
if (produce) {
|
||||
String privAlias = require(cli, PRIV_OPT, "signature produce requires --priv <alias>");
|
||||
PrivateKey priv = keyring.getPrivate(privAlias);
|
||||
tail = new TagTrailerDataContentBuilder<>(
|
||||
TagEngineBuilder.signature(session, alg, priv, spec)).build(true);
|
||||
} else {
|
||||
String pubAlias = require(cli, PUB_OPT, "signature verify requires --pub <alias>");
|
||||
PublicKey pub = keyring.getPublic(pubAlias);
|
||||
tail = new TagTrailerDataContentBuilder<>(
|
||||
TagEngineBuilder.signature(session, alg, pub, spec)).build(false);
|
||||
}
|
||||
}
|
||||
} else { // digest
|
||||
DigestSpec spec = parseDigest(alg);
|
||||
|
||||
@@ -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