mirror of
https://codeberg.org/PGPainless/sop-java.git
synced 2024-12-22 04:57:56 +01:00
Merge pull request #23 from pgpainless/kotlinCmd
Rewrite sop-java-picocli classes in Kotlin
This commit is contained in:
commit
a5c332737b
45 changed files with 1594 additions and 1834 deletions
|
@ -1,14 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,129 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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 <a href="https://picocli.info/#_controlling_the_locale">Picocli Readme</a>
|
|
||||||
*/
|
|
||||||
class InitLocale {
|
|
||||||
@CommandLine.Option(names = { "-l", "--locale" }, descriptionKey = "sop.locale")
|
|
||||||
void setLocale(String locale) {
|
|
||||||
Locale.setDefault(new Locale(locale));
|
|
||||||
}
|
|
||||||
|
|
||||||
@CommandLine.Unmatched
|
|
||||||
List<String> remainder; // ignore any other parameters and options in the first parsing phase
|
|
||||||
}
|
|
|
@ -1,282 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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<String> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,255 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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<String> withSessionKey = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(
|
|
||||||
names = {OPT_WITH_PASSWORD},
|
|
||||||
paramLabel = "PASSWORD")
|
|
||||||
List<String> 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<String> 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<String> keys = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = {OPT_WITH_KEY_PASSWORD},
|
|
||||||
paramLabel = "PASSWORD")
|
|
||||||
List<String> 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<DecryptionResult> 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<String> 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<String> 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<String> 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<String> 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<String> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,154 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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<String> withPassword = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--sign-with",
|
|
||||||
paramLabel = "KEY")
|
|
||||||
List<String> signWith = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--with-key-password",
|
|
||||||
paramLabel = "PASSWORD")
|
|
||||||
List<String> withKeyPassword = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--profile",
|
|
||||||
paramLabel = "PROFILE")
|
|
||||||
String profile;
|
|
||||||
|
|
||||||
@CommandLine.Parameters(index = "0..*",
|
|
||||||
paramLabel = "CERTS")
|
|
||||||
List<String> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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<String> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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<String> secretKeyFile = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--with-key-password",
|
|
||||||
paramLabel = "PASSWORD")
|
|
||||||
List<String> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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<String> 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<Verification> verifications = null;
|
|
||||||
try {
|
|
||||||
ReadyWithResult<List<Verification>> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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<String> secretKeyFile = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--with-key-password",
|
|
||||||
paramLabel = "PASSWORD")
|
|
||||||
List<String> 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<SigningResult> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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<String> 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<Verification> 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subcommands of the PGPainless SOP.
|
|
||||||
*/
|
|
||||||
package sop.cli.picocli.commands;
|
|
|
@ -1,8 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the Stateless OpenPGP Command Line Interface using Picocli.
|
|
||||||
*/
|
|
||||||
package sop.cli.picocli;
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
113
sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt
Normal file
113
sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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 <a href="https://picocli.info/#_controlling_the_locale">Picocli Readme</a>
|
||||||
|
*/
|
||||||
|
@Command
|
||||||
|
class InitLocale {
|
||||||
|
@Option(names = ["-l", "--locale"], descriptionKey = "sop.locale")
|
||||||
|
fun setLocale(locale: String) = Locale.setDefault(Locale(locale))
|
||||||
|
|
||||||
|
@Unmatched
|
||||||
|
var remainder: MutableList<String> =
|
||||||
|
mutableListOf() // ignore any other parameters and options in the first parsing phase
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,248 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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 <T> 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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<String> = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,227 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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<String> = listOf()
|
||||||
|
|
||||||
|
@Option(names = [OPT_WITH_PASSWORD], paramLabel = "PASSWORD")
|
||||||
|
var withPassword: List<String> = listOf()
|
||||||
|
|
||||||
|
@Option(names = [OPT_VERIFICATIONS_OUT], paramLabel = "VERIFICATIONS")
|
||||||
|
var verifyOut: String? = null
|
||||||
|
|
||||||
|
@Option(names = [OPT_VERIFY_WITH], paramLabel = "CERT") var certs: List<String> = 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<String> = listOf()
|
||||||
|
|
||||||
|
@Option(names = [OPT_WITH_KEY_PASSWORD], paramLabel = "PASSWORD")
|
||||||
|
var withKeyPassword: List<String> = 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<String>, 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<String>, 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<String>, 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<String>, 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<String>, 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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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<String> = listOf()
|
||||||
|
|
||||||
|
@Option(names = ["--sign-with"], paramLabel = "KEY") var signWith: List<String> = listOf()
|
||||||
|
|
||||||
|
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
|
||||||
|
var withKeyPassword: List<String> = listOf()
|
||||||
|
|
||||||
|
@Option(names = ["--profile"], paramLabel = "PROFILE") var profile: String? = null
|
||||||
|
|
||||||
|
@Parameters(index = "0..*", paramLabel = "CERTS") var certs: List<String> = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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<String> = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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<String> = listOf()
|
||||||
|
|
||||||
|
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
|
||||||
|
var withKeyPassword: List<String> = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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<String> = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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<String> = listOf()
|
||||||
|
|
||||||
|
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
|
||||||
|
var withKeyPassword: List<String> = 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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<String>
|
||||||
|
|
||||||
|
@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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,7 +45,7 @@ public class SOPTest {
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(1)
|
@ExpectSystemExitWithStatus(1)
|
||||||
public void assertThrowsIfNoSOPBackendSet() {
|
public void assertThrowsIfNoSOPBackendSet() {
|
||||||
SopCLI.SOP_INSTANCE = null;
|
SopCLI.setSopInstance(null);
|
||||||
// At this point, no SOP backend is set, so an InvalidStateException triggers exit(1)
|
// At this point, no SOP backend is set, so an InvalidStateException triggers exit(1)
|
||||||
SopCLI.main(new String[] {"armor"});
|
SopCLI.main(new String[] {"armor"});
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ public class AbstractSopCmdTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getInput_NullInvalid() {
|
public void getInput_NullInvalid() {
|
||||||
assertThrows(IllegalArgumentException.class, () -> abstractCmd.getInput(null));
|
assertThrows(NullPointerException.class, () -> abstractCmd.getInput(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -278,7 +278,7 @@ public class DecryptCmdTest {
|
||||||
File certFile = File.createTempFile("existing-verify-out-cert", ".asc");
|
File certFile = File.createTempFile("existing-verify-out-cert", ".asc");
|
||||||
File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp");
|
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
|
@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))) {
|
try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) {
|
||||||
String line = reader.readLine();
|
String line = reader.readLine();
|
||||||
assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line);
|
assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line);
|
||||||
|
@ -377,6 +377,6 @@ public class DecryptCmdTest {
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.IncompleteVerification.EXIT_CODE)
|
@ExpectSystemExitWithStatus(SOPGPException.IncompleteVerification.EXIT_CODE)
|
||||||
public void verifyOutWithoutVerifyWithCausesExit23() {
|
public void verifyOutWithoutVerifyWithCausesExit23() {
|
||||||
SopCLI.main(new String[] {"decrypt", "--verify-out", "out.file"});
|
SopCLI.main(new String[] {"decrypt", "--verifications-out", "out.file"});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* 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.
|
* target output stream is not yet known.
|
||||||
*/
|
*/
|
||||||
class ProxyOutputStream {
|
class ProxyOutputStream : OutputStream() {
|
||||||
private val buffer = ByteArrayOutputStream()
|
private val buffer = ByteArrayOutputStream()
|
||||||
private var swapped: OutputStream? = null
|
private var swapped: OutputStream? = null
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class ProxyOutputStream {
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun write(b: ByteArray) {
|
override fun write(b: ByteArray) {
|
||||||
if (swapped == null) {
|
if (swapped == null) {
|
||||||
buffer.write(b)
|
buffer.write(b)
|
||||||
} else {
|
} else {
|
||||||
|
@ -37,7 +37,7 @@ class ProxyOutputStream {
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun write(b: ByteArray, off: Int, len: Int) {
|
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||||
if (swapped == null) {
|
if (swapped == null) {
|
||||||
buffer.write(b, off, len)
|
buffer.write(b, off, len)
|
||||||
} else {
|
} else {
|
||||||
|
@ -47,7 +47,7 @@ class ProxyOutputStream {
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun flush() {
|
override fun flush() {
|
||||||
buffer.flush()
|
buffer.flush()
|
||||||
if (swapped != null) {
|
if (swapped != null) {
|
||||||
swapped!!.flush()
|
swapped!!.flush()
|
||||||
|
@ -56,7 +56,7 @@ class ProxyOutputStream {
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun close() {
|
override fun close() {
|
||||||
buffer.close()
|
buffer.close()
|
||||||
if (swapped != null) {
|
if (swapped != null) {
|
||||||
swapped!!.close()
|
swapped!!.close()
|
||||||
|
@ -65,7 +65,7 @@ class ProxyOutputStream {
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun write(i: Int) {
|
override fun write(i: Int) {
|
||||||
if (swapped == null) {
|
if (swapped == null) {
|
||||||
buffer.write(i)
|
buffer.write(i)
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in a new issue