diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java b/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java deleted file mode 100644 index 9e81b66..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -public class Print { - - public static void outln(String string) { - // CHECKSTYLE:OFF - System.out.println(string); - // CHECKSTYLE:ON - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java deleted file mode 100644 index 8b38af3..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.CommandLine; -import sop.exception.SOPGPException; - -public class SOPExceptionExitCodeMapper implements CommandLine.IExitCodeExceptionMapper { - - @Override - public int getExitCode(Throwable exception) { - if (exception instanceof SOPGPException) { - return ((SOPGPException) exception).getExitCode(); - } - if (exception instanceof CommandLine.UnmatchedArgumentException) { - CommandLine.UnmatchedArgumentException ex = (CommandLine.UnmatchedArgumentException) exception; - // Unmatched option of subcommand (eg. `generate-key -k`) - if (ex.isUnknownOption()) { - return SOPGPException.UnsupportedOption.EXIT_CODE; - } - // Unmatched subcommand - return SOPGPException.UnsupportedSubcommand.EXIT_CODE; - } - // Invalid option (eg. `--label Invalid`) - if (exception instanceof CommandLine.ParameterException) { - return SOPGPException.UnsupportedOption.EXIT_CODE; - } - - // Others, like IOException etc. - return 1; - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java deleted file mode 100644 index f6906ff..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.CommandLine; - -public class SOPExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler { - - @Override - public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) { - - int exitCode = commandLine.getExitCodeExceptionMapper() != null ? - commandLine.getExitCodeExceptionMapper().getExitCode(ex) : - commandLine.getCommandSpec().exitCodeOnExecutionException(); - - CommandLine.Help.ColorScheme colorScheme = commandLine.getColorScheme(); - // CHECKSTYLE:OFF - if (ex.getMessage() != null) { - commandLine.getErr().println(colorScheme.errorText(ex.getMessage())); - } else { - commandLine.getErr().println(ex.getClass().getName()); - } - - if (SopCLI.stacktrace) { - ex.printStackTrace(commandLine.getErr()); - } - // CHECKSTYLE:ON - - return exitCode; - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java deleted file mode 100644 index 5420dea..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.AutoComplete; -import picocli.CommandLine; -import sop.SOP; -import sop.cli.picocli.commands.ArmorCmd; -import sop.cli.picocli.commands.ChangeKeyPasswordCmd; -import sop.cli.picocli.commands.DearmorCmd; -import sop.cli.picocli.commands.DecryptCmd; -import sop.cli.picocli.commands.InlineDetachCmd; -import sop.cli.picocli.commands.EncryptCmd; -import sop.cli.picocli.commands.ExtractCertCmd; -import sop.cli.picocli.commands.GenerateKeyCmd; -import sop.cli.picocli.commands.InlineSignCmd; -import sop.cli.picocli.commands.InlineVerifyCmd; -import sop.cli.picocli.commands.ListProfilesCmd; -import sop.cli.picocli.commands.RevokeKeyCmd; -import sop.cli.picocli.commands.SignCmd; -import sop.cli.picocli.commands.VerifyCmd; -import sop.cli.picocli.commands.VersionCmd; -import sop.exception.SOPGPException; - -import java.util.List; -import java.util.Locale; -import java.util.ResourceBundle; - -@CommandLine.Command( - name = "sop", - resourceBundle = "msg_sop", - exitCodeOnInvalidInput = SOPGPException.UnsupportedSubcommand.EXIT_CODE, - subcommands = { - // Meta Subcommands - VersionCmd.class, - ListProfilesCmd.class, - // Key and Certificate Management Subcommands - GenerateKeyCmd.class, - ChangeKeyPasswordCmd.class, - RevokeKeyCmd.class, - ExtractCertCmd.class, - // Messaging Subcommands - SignCmd.class, - VerifyCmd.class, - EncryptCmd.class, - DecryptCmd.class, - InlineDetachCmd.class, - InlineSignCmd.class, - InlineVerifyCmd.class, - // Transport Subcommands - ArmorCmd.class, - DearmorCmd.class, - // Miscellaneous Subcommands - CommandLine.HelpCommand.class, - AutoComplete.GenerateCompletion.class - } -) -public class SopCLI { - // Singleton - static SOP SOP_INSTANCE; - static ResourceBundle cliMsg = ResourceBundle.getBundle("msg_sop"); - - public static String EXECUTABLE_NAME = "sop"; - - @CommandLine.Option(names = {"--stacktrace"}, - scope = CommandLine.ScopeType.INHERIT) - static boolean stacktrace; - - public static void main(String[] args) { - int exitCode = execute(args); - if (exitCode != 0) { - System.exit(exitCode); - } - } - - public static int execute(String[] args) { - - // Set locale - new CommandLine(new InitLocale()).parseArgs(args); - - // get error message bundle - cliMsg = ResourceBundle.getBundle("msg_sop"); - - // Prepare CLI - CommandLine cmd = new CommandLine(SopCLI.class); - - // explicitly set help command resource bundle - cmd.getSubcommands().get("help").setResourceBundle(ResourceBundle.getBundle("msg_help")); - - // Hide generate-completion command - cmd.getSubcommands().get("generate-completion").getCommandSpec().usageMessage().hidden(true); - - cmd.setCommandName(EXECUTABLE_NAME) - .setExecutionExceptionHandler(new SOPExecutionExceptionHandler()) - .setExitCodeExceptionMapper(new SOPExceptionExitCodeMapper()) - .setCaseInsensitiveEnumValuesAllowed(true); - - return cmd.execute(args); - } - - public static SOP getSop() { - if (SOP_INSTANCE == null) { - String errorMsg = cliMsg.getString("sop.error.runtime.no_backend_set"); - throw new IllegalStateException(errorMsg); - } - return SOP_INSTANCE; - } - - public static void setSopInstance(SOP instance) { - SOP_INSTANCE = instance; - } -} - -/** - * Control the locale. - * - * @see Picocli Readme - */ -class InitLocale { - @CommandLine.Option(names = { "-l", "--locale" }, descriptionKey = "sop.locale") - void setLocale(String locale) { - Locale.setDefault(new Locale(locale)); - } - - @CommandLine.Unmatched - List remainder; // ignore any other parameters and options in the first parsing phase -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java deleted file mode 100644 index 9aec5a7..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java +++ /dev/null @@ -1,282 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import sop.exception.SOPGPException; -import sop.util.UTCUtil; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.text.ParseException; -import java.util.Collection; -import java.util.Date; -import java.util.Locale; -import java.util.ResourceBundle; -import java.util.regex.Pattern; - -/** - * Abstract super class of SOP subcommands. - */ -public abstract class AbstractSopCmd implements Runnable { - - /** - * Interface to modularize resolving of environment variables. - */ - public interface EnvironmentVariableResolver { - /** - * Resolve the value of the given environment variable. - * Return null if the variable is not present. - * - * @param name name of the variable - * @return variable value or null - */ - String resolveEnvironmentVariable(String name); - } - - public static final String PRFX_ENV = "@ENV:"; - public static final String PRFX_FD = "@FD:"; - public static final Date BEGINNING_OF_TIME = new Date(0); - public static final Date END_OF_TIME = new Date(8640000000000000L); - - public static final Pattern PATTERN_FD = Pattern.compile("^\\d{1,20}$"); - - protected final ResourceBundle messages; - protected EnvironmentVariableResolver envResolver = System::getenv; - - public AbstractSopCmd() { - this(Locale.getDefault()); - } - - public AbstractSopCmd(@Nonnull Locale locale) { - messages = ResourceBundle.getBundle("msg_sop", locale); - } - - void throwIfOutputExists(String output) { - if (output == null) { - return; - } - - File outputFile = new File(output); - if (outputFile.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.output_file_already_exists", outputFile.getAbsolutePath()); - throw new SOPGPException.OutputExists(errorMsg); - } - } - - public String getMsg(String key) { - return messages.getString(key); - } - - public String getMsg(String key, String arg1) { - return String.format(messages.getString(key), arg1); - } - - public String getMsg(String key, String arg1, String arg2) { - return String.format(messages.getString(key), arg1, arg2); - } - - void throwIfMissingArg(Object arg, String argName) { - if (arg == null) { - String errorMsg = getMsg("sop.error.usage.argument_required", argName); - throw new SOPGPException.MissingArg(errorMsg); - } - } - - void throwIfEmptyParameters(Collection arg, String parmName) { - if (arg.isEmpty()) { - String errorMsg = getMsg("sop.error.usage.parameter_required", parmName); - throw new SOPGPException.MissingArg(errorMsg); - } - } - - T throwIfUnsupportedSubcommand(T subcommand, String subcommandName) { - if (subcommand == null) { - String errorMsg = getMsg("sop.error.feature_support.subcommand_not_supported", subcommandName); - throw new SOPGPException.UnsupportedSubcommand(errorMsg); - } - return subcommand; - } - - void setEnvironmentVariableResolver(EnvironmentVariableResolver envResolver) { - if (envResolver == null) { - throw new NullPointerException("Variable envResolver cannot be null."); - } - this.envResolver = envResolver; - } - - public InputStream getInput(String indirectInput) throws IOException { - if (indirectInput == null) { - throw new IllegalArgumentException("Input cannot not be null."); - } - - String trimmed = indirectInput.trim(); - if (trimmed.isEmpty()) { - throw new IllegalArgumentException("Input cannot be blank."); - } - - if (trimmed.startsWith(PRFX_ENV)) { - if (new File(trimmed).exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed); - throw new SOPGPException.AmbiguousInput(errorMsg); - } - - String envName = trimmed.substring(PRFX_ENV.length()); - String envValue = envResolver.resolveEnvironmentVariable(envName); - if (envValue == null) { - String errorMsg = getMsg("sop.error.indirect_data_type.environment_variable_not_set", envName); - throw new IllegalArgumentException(errorMsg); - } - - if (envValue.trim().isEmpty()) { - String errorMsg = getMsg("sop.error.indirect_data_type.environment_variable_empty", envName); - throw new IllegalArgumentException(errorMsg); - } - - return new ByteArrayInputStream(envValue.getBytes("UTF8")); - - } else if (trimmed.startsWith(PRFX_FD)) { - - if (new File(trimmed).exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed); - throw new SOPGPException.AmbiguousInput(errorMsg); - } - - File fdFile = fileDescriptorFromString(trimmed); - try { - FileInputStream fileIn = new FileInputStream(fdFile); - return fileIn; - } catch (FileNotFoundException e) { - String errorMsg = getMsg("sop.error.indirect_data_type.file_descriptor_not_found", fdFile.getAbsolutePath()); - throw new IOException(errorMsg, e); - } - } else { - File file = new File(trimmed); - if (!file.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.input_file_does_not_exist", file.getAbsolutePath()); - throw new SOPGPException.MissingInput(errorMsg); - } - - if (!file.isFile()) { - String errorMsg = getMsg("sop.error.indirect_data_type.input_not_a_file", file.getAbsolutePath()); - throw new SOPGPException.MissingInput(errorMsg); - } - - return new FileInputStream(file); - } - } - - public OutputStream getOutput(String indirectOutput) throws IOException { - if (indirectOutput == null) { - throw new IllegalArgumentException("Output cannot be null."); - } - - String trimmed = indirectOutput.trim(); - if (trimmed.isEmpty()) { - throw new IllegalArgumentException("Output cannot be blank."); - } - - // @ENV not allowed for output - if (trimmed.startsWith(PRFX_ENV)) { - String errorMsg = getMsg("sop.error.indirect_data_type.illegal_use_of_env_designator"); - throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg); - } - - // File Descriptor - if (trimmed.startsWith(PRFX_FD)) { - File fdFile = fileDescriptorFromString(trimmed); - try { - FileOutputStream fout = new FileOutputStream(fdFile); - return fout; - } catch (FileNotFoundException e) { - String errorMsg = getMsg("sop.error.indirect_data_type.file_descriptor_not_found", fdFile.getAbsolutePath()); - throw new IOException(errorMsg, e); - } - } - - File file = new File(trimmed); - if (file.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.output_file_already_exists", file.getAbsolutePath()); - throw new SOPGPException.OutputExists(errorMsg); - } - - if (!file.createNewFile()) { - String errorMsg = getMsg("sop.error.indirect_data_type.output_file_cannot_be_created", file.getAbsolutePath()); - throw new IOException(errorMsg); - } - - return new FileOutputStream(file); - } - - public File fileDescriptorFromString(String fdString) { - File fdDir = new File("/dev/fd/"); - if (!fdDir.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.designator_fd_not_supported"); - throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg); - } - String fdNumber = fdString.substring(PRFX_FD.length()); - if (!PATTERN_FD.matcher(fdNumber).matches()) { - throw new IllegalArgumentException("File descriptor must be a positive number."); - } - File descriptor = new File(fdDir, fdNumber); - return descriptor; - } - - public static String stringFromInputStream(InputStream inputStream) throws IOException { - try { - ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); - byte[] buf = new byte[4096]; int read; - while ((read = inputStream.read(buf)) != -1) { - byteOut.write(buf, 0, read); - } - // TODO: For decrypt operations we MUST accept non-UTF8 passwords - return UTF8Util.decodeUTF8(byteOut.toByteArray()); - } finally { - inputStream.close(); - } - } - - public Date parseNotAfter(String notAfter) { - if (notAfter.equals("now")) { - return new Date(); - } - - if (notAfter.equals("-")) { - return END_OF_TIME; - } - - try { - return UTCUtil.parseUTCDate(notAfter); - } catch (ParseException e) { - String errorMsg = getMsg("sop.error.input.malformed_not_after"); - throw new IllegalArgumentException(errorMsg); - } - } - - public Date parseNotBefore(String notBefore) { - if (notBefore.equals("now")) { - return new Date(); - } - - if (notBefore.equals("-")) { - return BEGINNING_OF_TIME; - } - - try { - return UTCUtil.parseUTCDate(notBefore); - } catch (ParseException e) { - String errorMsg = getMsg("sop.error.input.malformed_not_before"); - throw new IllegalArgumentException(errorMsg); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java deleted file mode 100644 index 5691686..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; -import sop.operation.Armor; - -import java.io.IOException; - -@CommandLine.Command(name = "armor", - resourceBundle = "msg_armor", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ArmorCmd extends AbstractSopCmd { - - @CommandLine.Option(names = {"--label"}, - paramLabel = "{auto|sig|key|cert|message}") - ArmorLabel label; - - @Override - public void run() { - Armor armor = throwIfUnsupportedSubcommand( - SopCLI.getSop().armor(), - "armor"); - - if (label != null) { - try { - armor.label(label); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--label"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - try { - Ready ready = armor.data(System.in); - ready.writeTo(System.out); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data"); - throw new SOPGPException.BadData(errorMsg, badData); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java deleted file mode 100644 index 5a6aa2a..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.ChangeKeyPassword; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "change-key-password", - resourceBundle = "msg_change-key-password", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ChangeKeyPasswordCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = {"--old-key-password"}, - paramLabel = "PASSWORD") - List oldKeyPasswords = new ArrayList<>(); - - @CommandLine.Option(names = {"--new-key-password"}, arity = "0..1", - paramLabel = "PASSWORD") - String newKeyPassword = null; - - @Override - public void run() { - ChangeKeyPassword changeKeyPassword = throwIfUnsupportedSubcommand( - SopCLI.getSop().changeKeyPassword(), "change-key-password"); - - if (!armor) { - changeKeyPassword.noArmor(); - } - - for (String oldKeyPassword : oldKeyPasswords) { - changeKeyPassword.oldKeyPassphrase(oldKeyPassword); - } - - if (newKeyPassword != null) { - changeKeyPassword.newKeyPassphrase(newKeyPassword); - } - - try { - changeKeyPassword.keys(System.in).writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java deleted file mode 100644 index f73e351..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Dearmor; - -import java.io.IOException; - -@CommandLine.Command(name = "dearmor", - resourceBundle = "msg_dearmor", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class DearmorCmd extends AbstractSopCmd { - - @Override - public void run() { - Dearmor dearmor = throwIfUnsupportedSubcommand( - SopCLI.getSop().dearmor(), "dearmor"); - - try { - dearmor.data(System.in) - .writeTo(System.out); - } catch (SOPGPException.BadData e) { - String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data"); - throw new SOPGPException.BadData(errorMsg, e); - } catch (IOException e) { - String msg = e.getMessage(); - if (msg == null) { - throw new RuntimeException(e); - } - - String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data"); - if (msg.equals("invalid armor") || - msg.equals("invalid armor header") || - msg.equals("inconsistent line endings in headers") || - msg.startsWith("unable to decode base64 data")) { - throw new SOPGPException.BadData(errorMsg, e); - } - - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java deleted file mode 100644 index a681b4d..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java +++ /dev/null @@ -1,255 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.Verification; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Decrypt; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -@CommandLine.Command(name = "decrypt", - resourceBundle = "msg_decrypt", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class DecryptCmd extends AbstractSopCmd { - - private static final String OPT_SESSION_KEY_OUT = "--session-key-out"; - private static final String OPT_WITH_SESSION_KEY = "--with-session-key"; - private static final String OPT_WITH_PASSWORD = "--with-password"; - private static final String OPT_WITH_KEY_PASSWORD = "--with-key-password"; - private static final String OPT_VERIFICATIONS_OUT = "--verifications-out"; // see SOP-05 - private static final String OPT_VERIFY_WITH = "--verify-with"; - private static final String OPT_NOT_BEFORE = "--verify-not-before"; - private static final String OPT_NOT_AFTER = "--verify-not-after"; - - - @CommandLine.Option( - names = {OPT_SESSION_KEY_OUT}, - paramLabel = "SESSIONKEY") - String sessionKeyOut; - - @CommandLine.Option( - names = {OPT_WITH_SESSION_KEY}, - paramLabel = "SESSIONKEY") - List withSessionKey = new ArrayList<>(); - - @CommandLine.Option( - names = {OPT_WITH_PASSWORD}, - paramLabel = "PASSWORD") - List withPassword = new ArrayList<>(); - - @CommandLine.Option(names = {OPT_VERIFICATIONS_OUT, "--verify-out"}, // TODO: Remove --verify-out in 06 - paramLabel = "VERIFICATIONS") - String verifyOut; - - @CommandLine.Option(names = {OPT_VERIFY_WITH}, - paramLabel = "CERT") - List certs = new ArrayList<>(); - - @CommandLine.Option(names = {OPT_NOT_BEFORE}, - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {OPT_NOT_AFTER}, - paramLabel = "DATE") - String notAfter = "now"; - - @CommandLine.Parameters(index = "0..*", - paramLabel = "KEY") - List keys = new ArrayList<>(); - - @CommandLine.Option(names = {OPT_WITH_KEY_PASSWORD}, - paramLabel = "PASSWORD") - List withKeyPassword = new ArrayList<>(); - - @Override - public void run() { - Decrypt decrypt = throwIfUnsupportedSubcommand( - SopCLI.getSop().decrypt(), "decrypt"); - - throwIfOutputExists(verifyOut); - throwIfOutputExists(sessionKeyOut); - - setNotAfter(notAfter, decrypt); - setNotBefore(notBefore, decrypt); - setWithPasswords(withPassword, decrypt); - setWithSessionKeys(withSessionKey, decrypt); - setWithKeyPassword(withKeyPassword, decrypt); - setVerifyWith(certs, decrypt); - setDecryptWith(keys, decrypt); - - if (verifyOut != null && certs.isEmpty()) { - String errorMsg = getMsg("sop.error.usage.option_requires_other_option", OPT_VERIFICATIONS_OUT, OPT_VERIFY_WITH); - throw new SOPGPException.IncompleteVerification(errorMsg); - } - - try { - ReadyWithResult ready = decrypt.ciphertext(System.in); - DecryptionResult result = ready.writeTo(System.out); - writeSessionKeyOut(result); - writeVerifyOut(result); - - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.stdin_not_a_message"); - throw new SOPGPException.BadData(errorMsg, badData); - } catch (SOPGPException.CannotDecrypt e) { - String errorMsg = getMsg("sop.error.runtime.cannot_decrypt_message"); - throw new SOPGPException.CannotDecrypt(errorMsg, e); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } - } - - private void writeVerifyOut(DecryptionResult result) throws IOException { - if (verifyOut != null) { - if (result.getVerifications().isEmpty()) { - String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found"); - throw new SOPGPException.NoSignature(errorMsg); - } - - try (OutputStream fileOut = getOutput(verifyOut)) { - PrintWriter writer = new PrintWriter(fileOut); - for (Verification verification : result.getVerifications()) { - // CHECKSTYLE:OFF - writer.println(verification.toString()); - // CHECKSTYLE:ON - } - writer.flush(); - } - } - } - - private void writeSessionKeyOut(DecryptionResult result) throws IOException { - if (sessionKeyOut == null) { - return; - } - try (OutputStream outputStream = getOutput(sessionKeyOut)) { - if (!result.getSessionKey().isPresent()) { - String errorMsg = getMsg("sop.error.runtime.no_session_key_extracted"); - throw new SOPGPException.UnsupportedOption(String.format(errorMsg, OPT_SESSION_KEY_OUT)); - } - SessionKey sessionKey = result.getSessionKey().get(); - PrintWriter writer = new PrintWriter(outputStream); - // CHECKSTYLE:OFF - writer.println(sessionKey.toString()); - // CHECKSTYLE:ON - writer.flush(); - } - } - - private void setDecryptWith(List keys, Decrypt decrypt) { - for (String key : keys) { - try (InputStream keyIn = getInput(key)) { - decrypt.withKey(keyIn); - } catch (SOPGPException.KeyIsProtected keyIsProtected) { - String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", key); - throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_private_key", key); - throw new SOPGPException.BadData(errorMsg, badData); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - private void setVerifyWith(List certs, Decrypt decrypt) { - for (String cert : certs) { - try (InputStream certIn = getInput(cert)) { - decrypt.verifyWithCert(certIn); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_certificate", cert); - throw new SOPGPException.BadData(errorMsg, badData); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } - } - } - - private void setWithSessionKeys(List withSessionKey, Decrypt decrypt) { - for (String sessionKeyFile : withSessionKey) { - String sessionKeyString; - try { - sessionKeyString = stringFromInputStream(getInput(sessionKeyFile)); - } catch (IOException e) { - throw new RuntimeException(e); - } - SessionKey sessionKey; - try { - sessionKey = SessionKey.fromString(sessionKeyString); - } catch (IllegalArgumentException e) { - String errorMsg = getMsg("sop.error.input.malformed_session_key"); - throw new IllegalArgumentException(errorMsg, e); - } - try { - decrypt.withSessionKey(sessionKey); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_SESSION_KEY); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - } - - private void setWithPasswords(List withPassword, Decrypt decrypt) { - for (String passwordFile : withPassword) { - try { - String password = stringFromInputStream(getInput(passwordFile)); - decrypt.withPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_PASSWORD); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - private void setWithKeyPassword(List withKeyPassword, Decrypt decrypt) { - for (String passwordFile : withKeyPassword) { - try { - String password = stringFromInputStream(getInput(passwordFile)); - decrypt.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_KEY_PASSWORD); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - private void setNotAfter(String notAfter, Decrypt decrypt) { - Date notAfterDate = parseNotAfter(notAfter); - try { - decrypt.verifyNotAfter(notAfterDate); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_AFTER); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - private void setNotBefore(String notBefore, Decrypt decrypt) { - Date notBeforeDate = parseNotBefore(notBefore); - try { - decrypt.verifyNotBefore(notBeforeDate); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_BEFORE); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java deleted file mode 100644 index efda26f..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.operation.Encrypt; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "encrypt", - resourceBundle = "msg_encrypt", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class EncryptCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = {"--as"}, - paramLabel = "{binary|text}") - EncryptAs type; - - @CommandLine.Option(names = "--with-password", - paramLabel = "PASSWORD") - List withPassword = new ArrayList<>(); - - @CommandLine.Option(names = "--sign-with", - paramLabel = "KEY") - List signWith = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - paramLabel = "PASSWORD") - List withKeyPassword = new ArrayList<>(); - - @CommandLine.Option(names = "--profile", - paramLabel = "PROFILE") - String profile; - - @CommandLine.Parameters(index = "0..*", - paramLabel = "CERTS") - List certs = new ArrayList<>(); - - @Override - public void run() { - Encrypt encrypt = throwIfUnsupportedSubcommand( - SopCLI.getSop().encrypt(), "encrypt"); - - if (profile != null) { - try { - encrypt.profile(profile); - } catch (SOPGPException.UnsupportedProfile e) { - String errorMsg = getMsg("sop.error.usage.profile_not_supported", "encrypt", profile); - throw new SOPGPException.UnsupportedProfile(errorMsg, e); - } - } - - if (type != null) { - try { - encrypt.mode(type); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - if (withPassword.isEmpty() && certs.isEmpty()) { - String errorMsg = getMsg("sop.error.usage.password_or_cert_required"); - throw new SOPGPException.MissingArg(errorMsg); - } - - for (String passwordFileName : withPassword) { - try { - String password = stringFromInputStream(getInput(passwordFileName)); - encrypt.withPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - for (String passwordFileName : withKeyPassword) { - try { - String password = stringFromInputStream(getInput(passwordFileName)); - encrypt.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - for (String keyInput : signWith) { - try (InputStream keyIn = getInput(keyInput)) { - encrypt.signWith(keyIn); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.KeyIsProtected keyIsProtected) { - String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput); - throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - String errorMsg = getMsg("sop.error.runtime.key_uses_unsupported_asymmetric_algorithm", keyInput); - throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo); - } catch (SOPGPException.KeyCannotSign keyCannotSign) { - String errorMsg = getMsg("sop.error.runtime.key_cannot_sign", keyInput); - throw new SOPGPException.KeyCannotSign(errorMsg, keyCannotSign); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - for (String certInput : certs) { - try (InputStream certIn = getInput(certInput)) { - encrypt.withCert(certIn); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - String errorMsg = getMsg("sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput); - throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo); - } catch (SOPGPException.CertCannotEncrypt certCannotEncrypt) { - String errorMsg = getMsg("sop.error.runtime.cert_cannot_encrypt", certInput); - throw new SOPGPException.CertCannotEncrypt(errorMsg, certCannotEncrypt); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - if (!armor) { - encrypt.noArmor(); - } - - try { - Ready ready = encrypt.plaintext(System.in); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java deleted file mode 100644 index 64a7a84..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.IOException; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.ExtractCert; - -@CommandLine.Command(name = "extract-cert", - resourceBundle = "msg_extract-cert", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ExtractCertCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @Override - public void run() { - ExtractCert extractCert = throwIfUnsupportedSubcommand( - SopCLI.getSop().extractCert(), "extract-cert"); - - if (!armor) { - extractCert.noArmor(); - } - - try { - Ready ready = extractCert.key(System.in); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.stdin_not_a_private_key"); - throw new SOPGPException.BadData(errorMsg, badData); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java deleted file mode 100644 index eea992e..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.GenerateKey; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "generate-key", - resourceBundle = "msg_generate-key", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class GenerateKeyCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Parameters(paramLabel = "USERID") - List userId = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - paramLabel = "PASSWORD") - String withKeyPassword; - - @CommandLine.Option(names = "--profile", - paramLabel = "PROFILE") - String profile; - - @CommandLine.Option(names = "--signing-only") - boolean signingOnly = false; - - @Override - public void run() { - GenerateKey generateKey = throwIfUnsupportedSubcommand( - SopCLI.getSop().generateKey(), "generate-key"); - - if (profile != null) { - try { - generateKey.profile(profile); - } catch (SOPGPException.UnsupportedProfile e) { - String errorMsg = getMsg("sop.error.usage.profile_not_supported", "generate-key", profile); - throw new SOPGPException.UnsupportedProfile(errorMsg, e); - } - } - - if (signingOnly) { - generateKey.signingOnly(); - } - - for (String userId : userId) { - generateKey.userId(userId); - } - - if (!armor) { - generateKey.noArmor(); - } - - if (withKeyPassword != null) { - try { - String password = stringFromInputStream(getInput(withKeyPassword)); - generateKey.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption e) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, e); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - try { - Ready ready = generateKey.generate(); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java deleted file mode 100644 index 52b654f..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Signatures; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.InlineDetach; - -import java.io.IOException; -import java.io.OutputStream; - -@CommandLine.Command(name = "inline-detach", - resourceBundle = "msg_inline-detach", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class InlineDetachCmd extends AbstractSopCmd { - - @CommandLine.Option( - names = {"--signatures-out"}, - paramLabel = "SIGNATURES") - String signaturesOut; - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @Override - public void run() { - InlineDetach inlineDetach = throwIfUnsupportedSubcommand( - SopCLI.getSop().inlineDetach(), "inline-detach"); - - throwIfOutputExists(signaturesOut); - throwIfMissingArg(signaturesOut, "--signatures-out"); - - if (!armor) { - inlineDetach.noArmor(); - } - - try (OutputStream outputStream = getOutput(signaturesOut)) { - Signatures signatures = inlineDetach - .message(System.in).writeTo(System.out); - signatures.writeTo(outputStream); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java deleted file mode 100644 index 1865bcf..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.enums.InlineSignAs; -import sop.exception.SOPGPException; -import sop.operation.InlineSign; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "inline-sign", - resourceBundle = "msg_inline-sign", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class InlineSignCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = "--as", - paramLabel = "{binary|text|clearsigned}") - InlineSignAs type; - - @CommandLine.Parameters(paramLabel = "KEYS") - List secretKeyFile = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - paramLabel = "PASSWORD") - List withKeyPassword = new ArrayList<>(); - - @Override - public void run() { - InlineSign inlineSign = throwIfUnsupportedSubcommand( - SopCLI.getSop().inlineSign(), "inline-sign"); - - // Clearsigned messages are inherently armored, so --no-armor makes no sense. - if (!armor && type == InlineSignAs.clearsigned) { - String errorMsg = getMsg("sop.error.usage.incompatible_options.clearsigned_no_armor"); - throw new SOPGPException.IncompatibleOptions(errorMsg); - } - - if (type != null) { - try { - inlineSign.mode(type); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - if (secretKeyFile.isEmpty()) { - String errorMsg = getMsg("sop.error.usage.parameter_required", "KEYS"); - throw new SOPGPException.MissingArg(errorMsg); - } - - for (String passwordFile : withKeyPassword) { - try { - String password = stringFromInputStream(getInput(passwordFile)); - inlineSign.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - for (String keyInput : secretKeyFile) { - try (InputStream keyIn = getInput(keyInput)) { - inlineSign.key(keyIn); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.KeyIsProtected e) { - String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput); - throw new SOPGPException.KeyIsProtected(errorMsg, e); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - if (!armor) { - inlineSign.noArmor(); - } - - try { - Ready ready = inlineSign.data(System.in); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java deleted file mode 100644 index c413c85..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.ReadyWithResult; -import sop.Verification; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.InlineVerify; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "inline-verify", - resourceBundle = "msg_inline-verify", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class InlineVerifyCmd extends AbstractSopCmd { - - @CommandLine.Parameters(arity = "0..*", - paramLabel = "CERT") - List certificates = new ArrayList<>(); - - @CommandLine.Option(names = {"--not-before"}, - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {"--not-after"}, - paramLabel = "DATE") - String notAfter = "now"; - - @CommandLine.Option(names = "--verifications-out", paramLabel = "VERIFICATIONS") - String verificationsOut; - - @Override - public void run() { - InlineVerify inlineVerify = throwIfUnsupportedSubcommand( - SopCLI.getSop().inlineVerify(), "inline-verify"); - - throwIfOutputExists(verificationsOut); - - if (notAfter != null) { - try { - inlineVerify.notAfter(parseNotAfter(notAfter)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - if (notBefore != null) { - try { - inlineVerify.notBefore(parseNotBefore(notBefore)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - for (String certInput : certificates) { - try (InputStream certIn = getInput(certInput)) { - inlineVerify.cert(certIn); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - String errorMsg = getMsg("sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput); - throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - List verifications = null; - try { - ReadyWithResult> ready = inlineVerify.data(System.in); - verifications = ready.writeTo(System.out); - } catch (SOPGPException.NoSignature e) { - String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found"); - throw new SOPGPException.NoSignature(errorMsg, e); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.stdin_not_a_message"); - throw new SOPGPException.BadData(errorMsg, badData); - } - - if (verificationsOut != null) { - try (OutputStream outputStream = getOutput(verificationsOut)) { - PrintWriter pw = new PrintWriter(outputStream); - for (Verification verification : verifications) { - // CHECKSTYLE:OFF - pw.println(verification); - // CHECKSTYLE:ON - } - pw.flush(); - pw.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java deleted file mode 100644 index 53ec024..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Profile; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.ListProfiles; - -@CommandLine.Command(name = "list-profiles", - resourceBundle = "msg_list-profiles", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ListProfilesCmd extends AbstractSopCmd { - - @CommandLine.Parameters(paramLabel = "COMMAND", arity = "1", descriptionKey = "subcommand") - String subcommand; - - @Override - public void run() { - ListProfiles listProfiles = throwIfUnsupportedSubcommand( - SopCLI.getSop().listProfiles(), "list-profiles"); - - try { - for (Profile profile : listProfiles.subcommand(subcommand)) { - Print.outln(profile.toString()); - } - } catch (SOPGPException.UnsupportedProfile e) { - String errorMsg = getMsg("sop.error.feature_support.subcommand_does_not_support_profiles", subcommand); - throw new SOPGPException.UnsupportedProfile(errorMsg, e); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java deleted file mode 100644 index 45f22fa..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.RevokeKey; - -import java.io.IOException; - -@CommandLine.Command(name = "revoke-key", - resourceBundle = "msg_revoke-key", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class RevokeKeyCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = "--with-key-password", - paramLabel = "PASSWORD") - String withKeyPassword; - - @Override - public void run() { - RevokeKey revokeKey = throwIfUnsupportedSubcommand( - SopCLI.getSop().revokeKey(), "revoke-key"); - - if (!armor) { - revokeKey.noArmor(); - } - - if (withKeyPassword != null) { - try { - String password = stringFromInputStream(getInput(withKeyPassword)); - revokeKey.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption e) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, e); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - Ready ready; - try { - ready = revokeKey.keys(System.in); - } catch (SOPGPException.KeyIsProtected e) { - String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", "STANDARD_IN"); - throw new SOPGPException.KeyIsProtected(errorMsg, e); - } - try { - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java deleted file mode 100644 index cad9d6e..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.MicAlg; -import sop.ReadyWithResult; -import sop.SigningResult; -import sop.cli.picocli.SopCLI; -import sop.enums.SignAs; -import sop.exception.SOPGPException; -import sop.operation.DetachedSign; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "sign", - resourceBundle = "msg_detached-sign", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class SignCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = "--as", - paramLabel = "{binary|text}") - SignAs type; - - @CommandLine.Parameters(paramLabel = "KEYS") - List secretKeyFile = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - paramLabel = "PASSWORD") - List withKeyPassword = new ArrayList<>(); - - @CommandLine.Option(names = "--micalg-out", - paramLabel = "MICALG") - String micAlgOut; - - @Override - public void run() { - DetachedSign detachedSign = throwIfUnsupportedSubcommand( - SopCLI.getSop().detachedSign(), "sign"); - - throwIfOutputExists(micAlgOut); - throwIfEmptyParameters(secretKeyFile, "KEYS"); - - if (type != null) { - try { - detachedSign.mode(type); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - for (String passwordFile : withKeyPassword) { - try { - String password = stringFromInputStream(getInput(passwordFile)); - detachedSign.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - for (String keyInput : secretKeyFile) { - try (InputStream keyIn = getInput(keyInput)) { - detachedSign.key(keyIn); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.KeyIsProtected keyIsProtected) { - String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput); - throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - if (!armor) { - detachedSign.noArmor(); - } - - try { - ReadyWithResult ready = detachedSign.data(System.in); - SigningResult result = ready.writeTo(System.out); - - MicAlg micAlg = result.getMicAlg(); - if (micAlgOut != null) { - // Write micalg out - OutputStream outputStream = getOutput(micAlgOut); - micAlg.writeTo(outputStream); - outputStream.close(); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java deleted file mode 100644 index d76bb37..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Verification; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.DetachedVerify; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "verify", - resourceBundle = "msg_detached-verify", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class VerifyCmd extends AbstractSopCmd { - - @CommandLine.Parameters(index = "0", - paramLabel = "SIGNATURE") - String signature; - - @CommandLine.Parameters(index = "1..*", - arity = "1..*", - paramLabel = "CERT") - List certificates = new ArrayList<>(); - - @CommandLine.Option(names = {"--not-before"}, - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {"--not-after"}, - paramLabel = "DATE") - String notAfter = "now"; - - @Override - public void run() { - DetachedVerify detachedVerify = throwIfUnsupportedSubcommand( - SopCLI.getSop().detachedVerify(), "verify"); - - if (notAfter != null) { - try { - detachedVerify.notAfter(parseNotAfter(notAfter)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - if (notBefore != null) { - try { - detachedVerify.notBefore(parseNotBefore(notBefore)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - for (String certInput : certificates) { - try (InputStream certIn = getInput(certInput)) { - detachedVerify.cert(certIn); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - if (signature != null) { - try (InputStream sigIn = getInput(signature)) { - detachedVerify.signatures(sigIn); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_signature", signature); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - List verifications; - try { - verifications = detachedVerify.data(System.in); - } catch (SOPGPException.NoSignature e) { - String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found"); - throw new SOPGPException.NoSignature(errorMsg, e); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.stdin_not_a_message"); - throw new SOPGPException.BadData(errorMsg, badData); - } - - for (Verification verification : verifications) { - Print.outln(verification.toString()); - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java deleted file mode 100644 index 6ccb8f7..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Version; - -@CommandLine.Command(name = "version", resourceBundle = "msg_version", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class VersionCmd extends AbstractSopCmd { - - @CommandLine.ArgGroup() - Exclusive exclusive; - - static class Exclusive { - @CommandLine.Option(names = "--extended") - boolean extended; - - @CommandLine.Option(names = "--backend") - boolean backend; - - @CommandLine.Option(names = "--sop-spec") - boolean sopSpec; - } - - - - @Override - public void run() { - Version version = throwIfUnsupportedSubcommand( - SopCLI.getSop().version(), "version"); - - if (exclusive == null) { - Print.outln(version.getName() + " " + version.getVersion()); - return; - } - - if (exclusive.extended) { - Print.outln(version.getExtendedVersion()); - return; - } - - if (exclusive.backend) { - Print.outln(version.getBackendVersion()); - return; - } - - if (exclusive.sopSpec) { - Print.outln(version.getSopSpecVersion()); - return; - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java deleted file mode 100644 index fc6aefd..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Subcommands of the PGPainless SOP. - */ -package sop.cli.picocli.commands; diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java deleted file mode 100644 index 83f426d..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Implementation of the Stateless OpenPGP Command Line Interface using Picocli. - */ -package sop.cli.picocli; diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt new file mode 100644 index 0000000..29aa77b --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli + +import picocli.CommandLine.* +import sop.exception.SOPGPException + +class SOPExceptionExitCodeMapper : IExitCodeExceptionMapper { + + override fun getExitCode(exception: Throwable): Int = + if (exception is SOPGPException) { + // SOPGPExceptions have well-defined exit code + exception.getExitCode() + } else if (exception is UnmatchedArgumentException) { + if (exception.isUnknownOption) { + // Unmatched option of subcommand (e.g. `generate-key --unknown`) + SOPGPException.UnsupportedOption.EXIT_CODE + } else { + // Unmatched subcommand + SOPGPException.UnsupportedSubcommand.EXIT_CODE + } + } else if (exception is ParameterException) { + // Invalid option (e.g. `--as invalid`) + SOPGPException.UnsupportedOption.EXIT_CODE + } else { + // Others, like IOException etc. + 1 + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExecutionExceptionHandler.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExecutionExceptionHandler.kt new file mode 100644 index 0000000..52236d3 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExecutionExceptionHandler.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli + +import picocli.CommandLine +import picocli.CommandLine.IExecutionExceptionHandler + +class SOPExecutionExceptionHandler : IExecutionExceptionHandler { + override fun handleExecutionException( + ex: Exception, + commandLine: CommandLine, + parseResult: CommandLine.ParseResult + ): Int { + val exitCode = + if (commandLine.exitCodeExceptionMapper != null) + commandLine.exitCodeExceptionMapper.getExitCode(ex) + else commandLine.commandSpec.exitCodeOnExecutionException() + + val colorScheme = commandLine.colorScheme + if (ex.message != null) { + commandLine.getErr().println(colorScheme.errorText(ex.message)) + } else { + commandLine.getErr().println(ex.javaClass.getName()) + } + + if (SopCLI.stacktrace) { + ex.printStackTrace(commandLine.getErr()) + } + + return exitCode + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt new file mode 100644 index 0000000..1d5d46b --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli + +import java.util.* +import kotlin.system.exitProcess +import picocli.AutoComplete.GenerateCompletion +import picocli.CommandLine +import picocli.CommandLine.* +import sop.SOP +import sop.cli.picocli.commands.* +import sop.exception.SOPGPException + +@Command( + name = "sop", + resourceBundle = "msg_sop", + exitCodeOnInvalidInput = SOPGPException.UnsupportedSubcommand.EXIT_CODE, + subcommands = + [ + // Meta subcommands + VersionCmd::class, + ListProfilesCmd::class, + // Key and certificate management + GenerateKeyCmd::class, + ChangeKeyPasswordCmd::class, + RevokeKeyCmd::class, + ExtractCertCmd::class, + // Messaging subcommands + SignCmd::class, + VerifyCmd::class, + EncryptCmd::class, + DecryptCmd::class, + InlineDetachCmd::class, + InlineSignCmd::class, + InlineVerifyCmd::class, + // Transport + ArmorCmd::class, + DearmorCmd::class, + // misc + HelpCommand::class, + GenerateCompletion::class]) +class SopCLI { + + companion object { + @JvmStatic private var sopInstance: SOP? = null + + @JvmStatic + fun getSop(): SOP = + checkNotNull(sopInstance) { cliMsg.getString("sop.error.runtime.no_backend_set") } + + @JvmStatic + fun setSopInstance(sop: SOP?) { + sopInstance = sop + } + + @JvmField var cliMsg: ResourceBundle = ResourceBundle.getBundle("msg_sop") + + @JvmField var EXECUTABLE_NAME = "sop" + + @JvmField + @Option(names = ["--stacktrace"], scope = CommandLine.ScopeType.INHERIT) + var stacktrace = false + + @JvmStatic + fun main(vararg args: String) { + val exitCode = execute(*args) + if (exitCode != 0) { + exitProcess(exitCode) + } + } + + @JvmStatic + fun execute(vararg args: String): Int { + // Set locale + CommandLine(InitLocale()).parseArgs(*args) + + // Re-set bundle with updated locale + cliMsg = ResourceBundle.getBundle("msg_sop") + + return CommandLine(SopCLI::class.java) + .apply { + // explicitly set help command resource bundle + subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help")) + // Hide generate-completion command + subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true) + // overwrite executable name + commandName = EXECUTABLE_NAME + // setup exception handling + executionExceptionHandler = SOPExecutionExceptionHandler() + exitCodeExceptionMapper = SOPExceptionExitCodeMapper() + isCaseInsensitiveEnumValuesAllowed = true + } + .execute(*args) + } + } + + /** + * Control the locale. + * + * @see Picocli Readme + */ + @Command + class InitLocale { + @Option(names = ["-l", "--locale"], descriptionKey = "sop.locale") + fun setLocale(locale: String) = Locale.setDefault(Locale(locale)) + + @Unmatched + var remainder: MutableList = + mutableListOf() // ignore any other parameters and options in the first parsing phase + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt new file mode 100644 index 0000000..4629e57 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.* +import java.text.ParseException +import java.util.* +import sop.cli.picocli.commands.AbstractSopCmd.EnvironmentVariableResolver +import sop.exception.SOPGPException.* +import sop.util.UTCUtil.Companion.parseUTCDate +import sop.util.UTF8Util.Companion.decodeUTF8 + +/** Abstract super class of SOP subcommands. */ +abstract class AbstractSopCmd(locale: Locale = Locale.getDefault()) : Runnable { + + private val messages: ResourceBundle = ResourceBundle.getBundle("msg_sop", locale) + var environmentVariableResolver = EnvironmentVariableResolver { name: String -> + System.getenv(name) + } + + /** Interface to modularize resolving of environment variables. */ + fun interface EnvironmentVariableResolver { + + /** + * Resolve the value of the given environment variable. Return null if the variable is not + * present. + * + * @param name name of the variable + * @return variable value or null + */ + fun resolveEnvironmentVariable(name: String): String? + } + + fun throwIfOutputExists(output: String?) { + output + ?.let { File(it) } + ?.let { + if (it.exists()) { + val errorMsg: String = + getMsg( + "sop.error.indirect_data_type.output_file_already_exists", + it.absolutePath) + throw OutputExists(errorMsg) + } + } + } + + fun getMsg(key: String): String = messages.getString(key) + + fun getMsg(key: String, vararg args: String): String { + val msg = messages.getString(key) + return String.format(msg, *args) + } + + fun throwIfMissingArg(arg: Any?, argName: String) { + if (arg == null) { + val errorMsg = getMsg("sop.error.usage.argument_required", argName) + throw MissingArg(errorMsg) + } + } + + fun throwIfEmptyParameters(arg: Collection<*>, parmName: String) { + if (arg.isEmpty()) { + val errorMsg = getMsg("sop.error.usage.parameter_required", parmName) + throw MissingArg(errorMsg) + } + } + + fun throwIfUnsupportedSubcommand(subcommand: T?, subcommandName: String): T { + if (subcommand == null) { + val errorMsg = + getMsg("sop.error.feature_support.subcommand_not_supported", subcommandName) + throw UnsupportedSubcommand(errorMsg) + } + return subcommand + } + + @Throws(IOException::class) + fun getInput(indirectInput: String): InputStream { + val trimmed = indirectInput.trim() + require(trimmed.isNotBlank()) { "Input cannot be blank." } + + if (trimmed.startsWith(PRFX_ENV)) { + if (File(trimmed).exists()) { + val errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed) + throw AmbiguousInput(errorMsg) + } + + val envName = trimmed.substring(PRFX_ENV.length) + val envValue = environmentVariableResolver.resolveEnvironmentVariable(envName) + requireNotNull(envValue) { + getMsg("sop.error.indirect_data_type.environment_variable_not_set", envName) + } + + require(envValue.trim().isNotEmpty()) { + getMsg("sop.error.indirect_data_type.environment_variable_empty", envName) + } + + return envValue.byteInputStream() + } else if (trimmed.startsWith(PRFX_FD)) { + + if (File(trimmed).exists()) { + val errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed) + throw AmbiguousInput(errorMsg) + } + + val fdFile: File = fileDescriptorFromString(trimmed) + return try { + fdFile.inputStream() + } catch (e: FileNotFoundException) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.file_descriptor_not_found", + fdFile.absolutePath) + throw IOException(errorMsg, e) + } + } else { + + val file = File(trimmed) + if (!file.exists()) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.input_file_does_not_exist", file.absolutePath) + throw MissingInput(errorMsg) + } + if (!file.isFile()) { + val errorMsg = + getMsg("sop.error.indirect_data_type.input_not_a_file", file.absolutePath) + throw MissingInput(errorMsg) + } + return file.inputStream() + } + } + + @Throws(IOException::class) + fun getOutput(indirectOutput: String?): OutputStream { + requireNotNull(indirectOutput) { "Output cannot be null." } + val trimmed = indirectOutput.trim() + require(trimmed.isNotEmpty()) { "Output cannot be blank." } + + // @ENV not allowed for output + if (trimmed.startsWith(PRFX_ENV)) { + val errorMsg = getMsg("sop.error.indirect_data_type.illegal_use_of_env_designator") + throw UnsupportedSpecialPrefix(errorMsg) + } + + // File Descriptor + if (trimmed.startsWith(PRFX_FD)) { + val fdFile = fileDescriptorFromString(trimmed) + return try { + fdFile.outputStream() + } catch (e: FileNotFoundException) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.file_descriptor_not_found", + fdFile.absolutePath) + throw IOException(errorMsg, e) + } + } + val file = File(trimmed) + if (file.exists()) { + val errorMsg = + getMsg("sop.error.indirect_data_type.output_file_already_exists", file.absolutePath) + throw OutputExists(errorMsg) + } + if (!file.createNewFile()) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.output_file_cannot_be_created", file.absolutePath) + throw IOException(errorMsg) + } + return file.outputStream() + } + + fun fileDescriptorFromString(fdString: String): File { + val fdDir = File("/dev/fd/") + if (!fdDir.exists()) { + val errorMsg = getMsg("sop.error.indirect_data_type.designator_fd_not_supported") + throw UnsupportedSpecialPrefix(errorMsg) + } + val fdNumber = fdString.substring(PRFX_FD.length) + require(PATTERN_FD.matcher(fdNumber).matches()) { + "File descriptor must be a positive number." + } + return File(fdDir, fdNumber) + } + + fun parseNotAfter(notAfter: String): Date { + return when (notAfter) { + "now" -> Date() + "-" -> END_OF_TIME + else -> + try { + parseUTCDate(notAfter) + } catch (e: ParseException) { + val errorMsg = getMsg("sop.error.input.malformed_not_after") + throw IllegalArgumentException(errorMsg) + } + } + } + + fun parseNotBefore(notBefore: String): Date { + return when (notBefore) { + "now" -> Date() + "-" -> DAWN_OF_TIME + else -> + try { + parseUTCDate(notBefore) + } catch (e: ParseException) { + val errorMsg = getMsg("sop.error.input.malformed_not_before") + throw IllegalArgumentException(errorMsg) + } + } + } + + companion object { + const val PRFX_ENV = "@ENV:" + + const val PRFX_FD = "@FD:" + + @JvmField val DAWN_OF_TIME = Date(0) + + @JvmField + @Deprecated("Replace with DAWN_OF_TIME", ReplaceWith("DAWN_OF_TIME")) + val BEGINNING_OF_TIME = DAWN_OF_TIME + + @JvmField val END_OF_TIME = Date(8640000000000000L) + + @JvmField val PATTERN_FD = "^\\d{1,20}$".toPattern() + + @Throws(IOException::class) + @JvmStatic + fun stringFromInputStream(inputStream: InputStream): String { + return inputStream.use { input -> + val byteOut = ByteArrayOutputStream() + val buf = ByteArray(4096) + var read: Int + while (input.read(buf).also { read = it } != -1) { + byteOut.write(buf, 0, read) + } + // TODO: For decrypt operations we MUST accept non-UTF8 passwords + decodeUTF8(byteOut.toByteArray()) + } + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt new file mode 100644 index 0000000..ccfba4d --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.enums.ArmorLabel +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.UnsupportedOption + +@Command( + name = "armor", + resourceBundle = "msg_armor", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class ArmorCmd : AbstractSopCmd() { + + @Option(names = ["--label"], paramLabel = "{auto|sig|key|cert|message}") + var label: ArmorLabel? = null + + override fun run() { + val armor = throwIfUnsupportedSubcommand(SopCLI.getSop().armor(), "armor") + + label?.let { + try { + armor.label(it) + } catch (unsupported: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--label") + throw UnsupportedOption(errorMsg, unsupported) + } + } + + try { + val ready = armor.data(System.`in`) + ready.writeTo(System.out) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data") + throw BadData(errorMsg, badData) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt new file mode 100644 index 0000000..0c2eb4a --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import java.lang.RuntimeException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException + +@Command( + name = "change-key-password", + resourceBundle = "msg_change-key-password", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class ChangeKeyPasswordCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true + + @Option(names = ["--old-key-password"], paramLabel = "PASSWORD") + var oldKeyPasswords: List = listOf() + + @Option(names = ["--new-key-password"], arity = "0..1", paramLabel = "PASSWORD") + var newKeyPassword: String? = null + + override fun run() { + val changeKeyPassword = + throwIfUnsupportedSubcommand(SopCLI.getSop().changeKeyPassword(), "change-key-password") + + if (!armor) { + changeKeyPassword.noArmor() + } + + oldKeyPasswords.forEach { changeKeyPassword.oldKeyPassphrase(it) } + + newKeyPassword?.let { changeKeyPassword.newKeyPassphrase(it) } + + try { + changeKeyPassword.keys(System.`in`).writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt new file mode 100644 index 0000000..09d2a71 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.exception.SOPGPException.BadData + +@Command( + name = "dearmor", + resourceBundle = "msg_dearmor", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class DearmorCmd : AbstractSopCmd() { + + override fun run() { + val dearmor = throwIfUnsupportedSubcommand(SopCLI.getSop().dearmor(), "dearmor") + + try { + dearmor.data(System.`in`).writeTo(System.out) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data") + throw BadData(errorMsg, badData) + } catch (e: IOException) { + e.message?.let { + val errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data") + if (it == "invalid armor" || + it == "invalid armor header" || + it == "inconsistent line endings in headers" || + it.startsWith("unable to decode base64 data")) { + throw BadData(errorMsg, e) + } + throw RuntimeException(e) + } + ?: throw RuntimeException(e) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt new file mode 100644 index 0000000..1e688b3 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import java.io.PrintWriter +import picocli.CommandLine.* +import sop.DecryptionResult +import sop.SessionKey +import sop.SessionKey.Companion.fromString +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.* +import sop.operation.Decrypt + +@Command( + name = "decrypt", + resourceBundle = "msg_decrypt", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class DecryptCmd : AbstractSopCmd() { + + @Option(names = [OPT_SESSION_KEY_OUT], paramLabel = "SESSIONKEY") + var sessionKeyOut: String? = null + + @Option(names = [OPT_WITH_SESSION_KEY], paramLabel = "SESSIONKEY") + var withSessionKey: List = listOf() + + @Option(names = [OPT_WITH_PASSWORD], paramLabel = "PASSWORD") + var withPassword: List = listOf() + + @Option(names = [OPT_VERIFICATIONS_OUT], paramLabel = "VERIFICATIONS") + var verifyOut: String? = null + + @Option(names = [OPT_VERIFY_WITH], paramLabel = "CERT") var certs: List = listOf() + + @Option(names = [OPT_NOT_BEFORE], paramLabel = "DATE") var notBefore = "-" + + @Option(names = [OPT_NOT_AFTER], paramLabel = "DATE") var notAfter = "now" + + @Parameters(index = "0..*", paramLabel = "KEY") var keys: List = listOf() + + @Option(names = [OPT_WITH_KEY_PASSWORD], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + override fun run() { + val decrypt = throwIfUnsupportedSubcommand(SopCLI.getSop().decrypt(), "decrypt") + + throwIfOutputExists(verifyOut) + throwIfOutputExists(sessionKeyOut) + + setNotAfter(notAfter, decrypt) + setNotBefore(notBefore, decrypt) + setWithPasswords(withPassword, decrypt) + setWithSessionKeys(withSessionKey, decrypt) + setWithKeyPassword(withKeyPassword, decrypt) + setVerifyWith(certs, decrypt) + setDecryptWith(keys, decrypt) + + if (verifyOut != null && certs.isEmpty()) { + val errorMsg = + getMsg( + "sop.error.usage.option_requires_other_option", + OPT_VERIFICATIONS_OUT, + OPT_VERIFY_WITH) + throw IncompleteVerification(errorMsg) + } + + try { + val ready = decrypt.ciphertext(System.`in`) + val result = ready.writeTo(System.out) + writeSessionKeyOut(result) + writeVerifyOut(result) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_a_message") + throw BadData(errorMsg, badData) + } catch (e: CannotDecrypt) { + val errorMsg = getMsg("sop.error.runtime.cannot_decrypt_message") + throw CannotDecrypt(errorMsg, e) + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } + } + + @Throws(IOException::class) + private fun writeVerifyOut(result: DecryptionResult) { + verifyOut?.let { + if (result.verifications.isEmpty()) { + val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found") + throw NoSignature(errorMsg) + } + + getOutput(verifyOut).use { out -> + PrintWriter(out).use { pw -> result.verifications.forEach { pw.println(it) } } + } + } + } + + @Throws(IOException::class) + private fun writeSessionKeyOut(result: DecryptionResult) { + sessionKeyOut?.let { fileName -> + getOutput(fileName).use { out -> + if (!result.sessionKey.isPresent) { + val errorMsg = getMsg("sop.error.runtime.no_session_key_extracted") + throw UnsupportedOption(String.format(errorMsg, OPT_SESSION_KEY_OUT)) + } + + PrintWriter(out).use { it.println(result.sessionKey.get()!!) } + } + } + } + + private fun setDecryptWith(keys: List, decrypt: Decrypt) { + for (key in keys) { + try { + getInput(key).use { decrypt.withKey(it) } + } catch (keyIsProtected: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", key) + throw KeyIsProtected(errorMsg, keyIsProtected) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", key) + throw BadData(errorMsg, badData) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + } + + private fun setVerifyWith(certs: List, decrypt: Decrypt) { + for (cert in certs) { + try { + getInput(cert).use { certIn -> decrypt.verifyWithCert(certIn) } + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", cert) + throw BadData(errorMsg, badData) + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } + } + } + + private fun setWithSessionKeys(withSessionKey: List, decrypt: Decrypt) { + for (sessionKeyFile in withSessionKey) { + val sessionKeyString: String = + try { + stringFromInputStream(getInput(sessionKeyFile)) + } catch (e: IOException) { + throw RuntimeException(e) + } + val sessionKey: SessionKey = + try { + fromString(sessionKeyString) + } catch (e: IllegalArgumentException) { + val errorMsg = getMsg("sop.error.input.malformed_session_key") + throw IllegalArgumentException(errorMsg, e) + } + try { + decrypt.withSessionKey(sessionKey) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_SESSION_KEY) + throw UnsupportedOption(errorMsg, unsupportedOption) + } + } + } + + private fun setWithPasswords(withPassword: List, decrypt: Decrypt) { + for (passwordFile in withPassword) { + try { + val password = stringFromInputStream(getInput(passwordFile)) + decrypt.withPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_PASSWORD) + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + } + + private fun setWithKeyPassword(withKeyPassword: List, decrypt: Decrypt) { + for (passwordFile in withKeyPassword) { + try { + val password = stringFromInputStream(getInput(passwordFile)) + decrypt.withKeyPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_KEY_PASSWORD) + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + } + + private fun setNotAfter(notAfter: String, decrypt: Decrypt) { + val notAfterDate = parseNotAfter(notAfter) + try { + decrypt.verifyNotAfter(notAfterDate) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_AFTER) + throw UnsupportedOption(errorMsg, unsupportedOption) + } + } + + private fun setNotBefore(notBefore: String, decrypt: Decrypt) { + val notBeforeDate = parseNotBefore(notBefore) + try { + decrypt.verifyNotBefore(notBeforeDate) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_BEFORE) + throw UnsupportedOption(errorMsg, unsupportedOption) + } + } + + companion object { + const val OPT_SESSION_KEY_OUT = "--session-key-out" + const val OPT_WITH_SESSION_KEY = "--with-session-key" + const val OPT_WITH_PASSWORD = "--with-password" + const val OPT_WITH_KEY_PASSWORD = "--with-key-password" + const val OPT_VERIFICATIONS_OUT = "--verifications-out" + const val OPT_VERIFY_WITH = "--verify-with" + const val OPT_NOT_BEFORE = "--verify-not-before" + const val OPT_NOT_AFTER = "--verify-not-after" + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt new file mode 100644 index 0000000..b3b0d87 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.enums.EncryptAs +import sop.exception.SOPGPException.* + +@Command( + name = "encrypt", + resourceBundle = "msg_encrypt", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class EncryptCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--as"], paramLabel = "{binary|text}") var type: EncryptAs? = null + + @Option(names = ["--with-password"], paramLabel = "PASSWORD") + var withPassword: List = listOf() + + @Option(names = ["--sign-with"], paramLabel = "KEY") var signWith: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + @Option(names = ["--profile"], paramLabel = "PROFILE") var profile: String? = null + + @Parameters(index = "0..*", paramLabel = "CERTS") var certs: List = listOf() + + override fun run() { + val encrypt = throwIfUnsupportedSubcommand(SopCLI.getSop().encrypt(), "encrypt") + + profile?.let { + try { + encrypt.profile(it) + } catch (e: UnsupportedProfile) { + val errorMsg = getMsg("sop.error.usage.profile_not_supported", "encrypt", it) + throw UnsupportedProfile(errorMsg, e) + } + } + + type?.let { + try { + encrypt.mode(it) + } catch (e: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as") + throw UnsupportedOption(errorMsg, e) + } + } + + if (withPassword.isEmpty() && certs.isEmpty()) { + val errorMsg = getMsg("sop.error.usage.password_or_cert_required") + throw MissingArg(errorMsg) + } + + for (passwordFileName in withPassword) { + try { + val password = stringFromInputStream(getInput(passwordFileName)) + encrypt.withPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-password") + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + for (passwordFileName in withKeyPassword) { + try { + val password = stringFromInputStream(getInput(passwordFileName)) + encrypt.withKeyPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + for (keyInput in signWith) { + try { + getInput(keyInput).use { keyIn -> encrypt.signWith(keyIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (keyIsProtected: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput) + throw KeyIsProtected(errorMsg, keyIsProtected) + } catch (unsupportedAsymmetricAlgo: UnsupportedAsymmetricAlgo) { + val errorMsg = + getMsg("sop.error.runtime.key_uses_unsupported_asymmetric_algorithm", keyInput) + throw UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo) + } catch (keyCannotSign: KeyCannotSign) { + val errorMsg = getMsg("sop.error.runtime.key_cannot_sign", keyInput) + throw KeyCannotSign(errorMsg, keyCannotSign) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput) + throw BadData(errorMsg, badData) + } + } + + for (certInput in certs) { + try { + getInput(certInput).use { certIn -> encrypt.withCert(certIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (unsupportedAsymmetricAlgo: UnsupportedAsymmetricAlgo) { + val errorMsg = + getMsg( + "sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput) + throw UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo) + } catch (certCannotEncrypt: CertCannotEncrypt) { + val errorMsg = getMsg("sop.error.runtime.cert_cannot_encrypt", certInput) + throw CertCannotEncrypt(errorMsg, certCannotEncrypt) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput) + throw BadData(errorMsg, badData) + } + } + + if (!armor) { + encrypt.noArmor() + } + + try { + val ready = encrypt.plaintext(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ExtractCertCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ExtractCertCmd.kt new file mode 100644 index 0000000..cff996f --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ExtractCertCmd.kt @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.exception.SOPGPException.BadData + +@Command( + name = "extract-cert", + resourceBundle = "msg_extract-cert", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class ExtractCertCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + override fun run() { + val extractCert = + throwIfUnsupportedSubcommand(SopCLI.getSop().extractCert(), "extract-cert") + + if (!armor) { + extractCert.noArmor() + } + + try { + val ready = extractCert.key(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_a_private_key") + throw BadData(errorMsg, badData) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt new file mode 100644 index 0000000..7fa5a70 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.UnsupportedOption +import sop.exception.SOPGPException.UnsupportedProfile + +@Command( + name = "generate-key", + resourceBundle = "msg_generate-key", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class GenerateKeyCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Parameters(paramLabel = "USERID") var userId: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: String? = null + + @Option(names = ["--profile"], paramLabel = "PROFILE") var profile: String? = null + + @Option(names = ["--signing-only"]) var signingOnly: Boolean = false + + override fun run() { + val generateKey = + throwIfUnsupportedSubcommand(SopCLI.getSop().generateKey(), "generate-key") + + profile?.let { + try { + generateKey.profile(it) + } catch (e: UnsupportedProfile) { + val errorMsg = + getMsg("sop.error.usage.profile_not_supported", "generate-key", profile!!) + throw UnsupportedProfile(errorMsg, e) + } + } + + if (signingOnly) { + generateKey.signingOnly() + } + + for (userId in userId) { + generateKey.userId(userId) + } + + if (!armor) { + generateKey.noArmor() + } + + withKeyPassword?.let { + try { + val password = stringFromInputStream(getInput(it)) + generateKey.withKeyPassword(password) + } catch (e: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw UnsupportedOption(errorMsg, e) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + try { + val ready = generateKey.generate() + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt new file mode 100644 index 0000000..e311adf --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import java.lang.RuntimeException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException + +@Command( + name = "inline-detach", + resourceBundle = "msg_inline-detach", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class InlineDetachCmd : AbstractSopCmd() { + + @Option(names = ["--signatures-out"], paramLabel = "SIGNATURES") + var signaturesOut: String? = null + + @Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true + + override fun run() { + val inlineDetach = + throwIfUnsupportedSubcommand(SopCLI.getSop().inlineDetach(), "inline-detach") + + throwIfOutputExists(signaturesOut) + throwIfMissingArg(signaturesOut, "--signatures-out") + + if (!armor) { + inlineDetach.noArmor() + } + + try { + getOutput(signaturesOut).use { sigOut -> + inlineDetach + .message(System.`in`) + .writeTo(System.out) // message out + .writeTo(sigOut) // signatures out + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt new file mode 100644 index 0000000..c41f6f6 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.enums.InlineSignAs +import sop.exception.SOPGPException.* + +@Command( + name = "inline-sign", + resourceBundle = "msg_inline-sign", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class InlineSignCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--as"], paramLabel = "{binary|text|clearsigned}") + var type: InlineSignAs? = null + + @Parameters(paramLabel = "KEYS") var secretKeyFile: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + override fun run() { + val inlineSign = throwIfUnsupportedSubcommand(SopCLI.getSop().inlineSign(), "inline-sign") + + if (!armor && type == InlineSignAs.clearsigned) { + val errorMsg = getMsg("sop.error.usage.incompatible_options.clearsigned_no_armor") + throw IncompatibleOptions(errorMsg) + } + + type?.let { + try { + inlineSign.mode(it) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + } + + if (secretKeyFile.isEmpty()) { + val errorMsg = getMsg("sop.error.usage.parameter_required", "KEYS") + throw MissingArg(errorMsg) + } + + for (passwordFile in withKeyPassword) { + try { + val password = stringFromInputStream(getInput(passwordFile)) + inlineSign.withKeyPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + for (keyInput in secretKeyFile) { + try { + getInput(keyInput).use { keyIn -> inlineSign.key(keyIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (e: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput) + throw KeyIsProtected(errorMsg, e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput) + throw BadData(errorMsg, badData) + } + } + + if (!armor) { + inlineSign.noArmor() + } + + try { + val ready = inlineSign.data(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineVerifyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineVerifyCmd.kt new file mode 100644 index 0000000..6a641a6 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineVerifyCmd.kt @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import java.io.PrintWriter +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.* + +@Command( + name = "inline-verify", + resourceBundle = "msg_inline-verify", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class InlineVerifyCmd : AbstractSopCmd() { + + @Parameters(arity = "0..*", paramLabel = "CERT") var certificates: List = listOf() + + @Option(names = ["--not-before"], paramLabel = "DATE") var notBefore: String = "-" + + @Option(names = ["--not-after"], paramLabel = "DATE") var notAfter: String = "now" + + @Option(names = ["--verifications-out"], paramLabel = "VERIFICATIONS") + var verificationsOut: String? = null + + override fun run() { + val inlineVerify = + throwIfUnsupportedSubcommand(SopCLI.getSop().inlineVerify(), "inline-verify") + + throwIfOutputExists(verificationsOut) + + try { + inlineVerify.notAfter(parseNotAfter(notAfter)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + try { + inlineVerify.notBefore(parseNotBefore(notBefore)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + for (certInput in certificates) { + try { + getInput(certInput).use { certIn -> inlineVerify.cert(certIn) } + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (unsupportedAsymmetricAlgo: UnsupportedAsymmetricAlgo) { + val errorMsg = + getMsg( + "sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput) + throw UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput) + throw BadData(errorMsg, badData) + } + } + + val verifications = + try { + val ready = inlineVerify.data(System.`in`) + ready.writeTo(System.out) + } catch (e: NoSignature) { + val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found") + throw NoSignature(errorMsg, e) + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_a_message") + throw BadData(errorMsg, badData) + } + + verificationsOut?.let { + try { + getOutput(it).use { outputStream -> + val pw = PrintWriter(outputStream) + for (verification in verifications) { + pw.println(verification) + } + pw.flush() + pw.close() + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt new file mode 100644 index 0000000..b770e82 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import picocli.CommandLine.Command +import picocli.CommandLine.Parameters +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.exception.SOPGPException.UnsupportedProfile + +@Command( + name = "list-profiles", + resourceBundle = "msg_list-profiles", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class ListProfilesCmd : AbstractSopCmd() { + + @Parameters(paramLabel = "COMMAND", arity = "1", descriptionKey = "subcommand") + lateinit var subcommand: String + + override fun run() { + val listProfiles = + throwIfUnsupportedSubcommand(SopCLI.getSop().listProfiles(), "list-profiles") + + try { + listProfiles.subcommand(subcommand).forEach { println(it) } + } catch (e: UnsupportedProfile) { + val errorMsg = + getMsg("sop.error.feature_support.subcommand_does_not_support_profiles", subcommand) + throw UnsupportedProfile(errorMsg, e) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt new file mode 100644 index 0000000..0b93ac5 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.exception.SOPGPException.KeyIsProtected + +@Command( + name = "revoke-key", + resourceBundle = "msg_revoke-key", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class RevokeKeyCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: String? = null + + override fun run() { + val revokeKey = throwIfUnsupportedSubcommand(SopCLI.getSop().revokeKey(), "revoke-key") + + if (!armor) { + revokeKey.noArmor() + } + + withKeyPassword?.let { + try { + val password = stringFromInputStream(getInput(it)) + revokeKey.withKeyPassword(password) + } catch (e: SOPGPException.UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw SOPGPException.UnsupportedOption(errorMsg, e) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + val ready = + try { + revokeKey.keys(System.`in`) + } catch (e: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", "STANDARD_IN") + throw KeyIsProtected(errorMsg, e) + } + try { + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/SignCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/SignCmd.kt new file mode 100644 index 0000000..6860477 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/SignCmd.kt @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.enums.SignAs +import sop.exception.SOPGPException +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.KeyIsProtected + +@Command( + name = "sign", + resourceBundle = "msg_detached-sign", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class SignCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true + + @Option(names = ["--as"], paramLabel = "{binary|text}") var type: SignAs? = null + + @Parameters(paramLabel = "KEYS") var secretKeyFile: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + @Option(names = ["--micalg-out"], paramLabel = "MICALG") var micAlgOut: String? = null + + override fun run() { + val detachedSign = throwIfUnsupportedSubcommand(SopCLI.getSop().detachedSign(), "sign") + + throwIfOutputExists(micAlgOut) + throwIfEmptyParameters(secretKeyFile, "KEYS") + + try { + type?.let { detachedSign.mode(it) } + } catch (unsupported: SOPGPException.UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw SOPGPException.UnsupportedOption(errorMsg, unsupported) + } catch (ioe: IOException) { + throw RuntimeException(ioe) + } + + withKeyPassword.forEach { passIn -> + try { + val password = stringFromInputStream(getInput(passIn)) + detachedSign.withKeyPassword(password) + } catch (unsupported: SOPGPException.UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw SOPGPException.UnsupportedOption(errorMsg, unsupported) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + secretKeyFile.forEach { keyIn -> + try { + getInput(keyIn).use { input -> detachedSign.key(input) } + } catch (ioe: IOException) { + throw RuntimeException(ioe) + } catch (keyIsProtected: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyIn) + throw KeyIsProtected(errorMsg, keyIsProtected) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", keyIn) + throw BadData(errorMsg, badData) + } + } + + if (!armor) { + detachedSign.noArmor() + } + + try { + val ready = detachedSign.data(System.`in`) + val result = ready.writeTo(System.out) + + if (micAlgOut != null) { + getOutput(micAlgOut).use { result.micAlg.writeTo(it) } + } + } catch (e: IOException) { + throw java.lang.RuntimeException(e) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VerifyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VerifyCmd.kt new file mode 100644 index 0000000..ef27266 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VerifyCmd.kt @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.* + +@Command( + name = "verify", + resourceBundle = "msg_detached-verify", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class VerifyCmd : AbstractSopCmd() { + + @Parameters(index = "0", paramLabel = "SIGNATURE") lateinit var signature: String + + @Parameters(index = "1..*", arity = "1..*", paramLabel = "CERT") + lateinit var certificates: List + + @Option(names = ["--not-before"], paramLabel = "DATE") var notBefore: String = "-" + + @Option(names = ["--not-after"], paramLabel = "DATE") var notAfter: String = "now" + + override fun run() { + val detachedVerify = + throwIfUnsupportedSubcommand(SopCLI.getSop().detachedVerify(), "verify") + try { + detachedVerify.notAfter(parseNotAfter(notAfter)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + try { + detachedVerify.notBefore(parseNotBefore(notBefore)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + for (certInput in certificates) { + try { + getInput(certInput).use { certIn -> detachedVerify.cert(certIn) } + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput) + throw BadData(errorMsg, badData) + } + } + + try { + getInput(signature).use { sigIn -> detachedVerify.signatures(sigIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_signature", signature) + throw BadData(errorMsg, badData) + } + + val verifications = + try { + detachedVerify.data(System.`in`) + } catch (e: NoSignature) { + val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found") + throw NoSignature(errorMsg, e) + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_a_message") + throw BadData(errorMsg, badData) + } + + for (verification in verifications) { + println(verification.toString()) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt new file mode 100644 index 0000000..75197fe --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import picocli.CommandLine.ArgGroup +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException + +@Command( + name = "version", + resourceBundle = "msg_version", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class VersionCmd : AbstractSopCmd() { + + @ArgGroup var exclusive: Exclusive? = null + + class Exclusive { + @Option(names = ["--extended"]) var extended: Boolean = false + @Option(names = ["--backend"]) var backend: Boolean = false + @Option(names = ["--sop-spec"]) var sopSpec: Boolean = false + } + + override fun run() { + val version = throwIfUnsupportedSubcommand(SopCLI.getSop().version(), "version") + + if (exclusive == null) { + // No option provided + println("${version.getName()} ${version.getVersion()}") + return + } + + if (exclusive!!.extended) { + println(version.getExtendedVersion()) + return + } + + if (exclusive!!.backend) { + println(version.getBackendVersion()) + return + } + + if (exclusive!!.sopSpec) { + println(version.getSopSpecVersion()) + return + } + } +} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java index 47b6123..68b32be 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java @@ -45,7 +45,7 @@ public class SOPTest { @Test @ExpectSystemExitWithStatus(1) public void assertThrowsIfNoSOPBackendSet() { - SopCLI.SOP_INSTANCE = null; + SopCLI.setSopInstance(null); // At this point, no SOP backend is set, so an InvalidStateException triggers exit(1) SopCLI.main(new String[] {"armor"}); } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java index aed420b..396bc7f 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java @@ -36,7 +36,7 @@ public class AbstractSopCmdTest { @Test public void getInput_NullInvalid() { - assertThrows(IllegalArgumentException.class, () -> abstractCmd.getInput(null)); + assertThrows(NullPointerException.class, () -> abstractCmd.getInput(null)); } @Test diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java index e3dd198..c2c67ba 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java @@ -278,7 +278,7 @@ public class DecryptCmdTest { File certFile = File.createTempFile("existing-verify-out-cert", ".asc"); File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp"); - SopCLI.main(new String[] {"decrypt", "--verify-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); + SopCLI.main(new String[] {"decrypt", "--verifications-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); } @Test @@ -302,7 +302,7 @@ public class DecryptCmdTest { } }); - SopCLI.main(new String[] {"decrypt", "--verify-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); + SopCLI.main(new String[] {"decrypt", "--verifications-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) { String line = reader.readLine(); assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line); @@ -377,6 +377,6 @@ public class DecryptCmdTest { @Test @ExpectSystemExitWithStatus(SOPGPException.IncompleteVerification.EXIT_CODE) public void verifyOutWithoutVerifyWithCausesExit23() { - SopCLI.main(new String[] {"decrypt", "--verify-out", "out.file"}); + SopCLI.main(new String[] {"decrypt", "--verifications-out", "out.file"}); } } diff --git a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt index 142f7b3..da6c4fa 100644 --- a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt +++ b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt @@ -15,7 +15,7 @@ import java.io.OutputStream * class is useful if we need to provide an [OutputStream] at one point in time when the final * target output stream is not yet known. */ -class ProxyOutputStream { +class ProxyOutputStream : OutputStream() { private val buffer = ByteArrayOutputStream() private var swapped: OutputStream? = null @@ -27,7 +27,7 @@ class ProxyOutputStream { @Synchronized @Throws(IOException::class) - fun write(b: ByteArray) { + override fun write(b: ByteArray) { if (swapped == null) { buffer.write(b) } else { @@ -37,7 +37,7 @@ class ProxyOutputStream { @Synchronized @Throws(IOException::class) - fun write(b: ByteArray, off: Int, len: Int) { + override fun write(b: ByteArray, off: Int, len: Int) { if (swapped == null) { buffer.write(b, off, len) } else { @@ -47,7 +47,7 @@ class ProxyOutputStream { @Synchronized @Throws(IOException::class) - fun flush() { + override fun flush() { buffer.flush() if (swapped != null) { swapped!!.flush() @@ -56,7 +56,7 @@ class ProxyOutputStream { @Synchronized @Throws(IOException::class) - fun close() { + override fun close() { buffer.close() if (swapped != null) { swapped!!.close() @@ -65,7 +65,7 @@ class ProxyOutputStream { @Synchronized @Throws(IOException::class) - fun write(i: Int) { + override fun write(i: Int) { if (swapped == null) { buffer.write(i) } else {