Wip: Refactor command implementation and enable i18n for CLI

This commit is contained in:
Paul Schaub 2022-06-06 20:06:14 +02:00
parent a7f02d58cc
commit 4934e472e2
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
34 changed files with 980 additions and 782 deletions

View file

@ -1,33 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli;
import java.util.Date;
import sop.util.UTCUtil;
public class DateParser {
public static final Date BEGINNING_OF_TIME = new Date(0);
public static final Date END_OF_TIME = new Date(8640000000000000L);
public static Date parseNotAfter(String notAfter) {
Date date = notAfter.equals("now") ? new Date() : notAfter.equals("-") ? END_OF_TIME : UTCUtil.parseUTCDate(notAfter);
if (date == null) {
Print.errln("Invalid date string supplied as value of --not-after.");
System.exit(1);
}
return date;
}
public static Date parseNotBefore(String notBefore) {
Date date = notBefore.equals("now") ? new Date() : notBefore.equals("-") ? BEGINNING_OF_TIME : UTCUtil.parseUTCDate(notBefore);
if (date == null) {
Print.errln("Invalid date string supplied as value of --not-before.");
System.exit(1);
}
return date;
}
}

View file

@ -1,115 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.IOException;
import sop.exception.SOPGPException;
import sop.util.UTF8Util;
public class FileUtil {
private static final String ERROR_AMBIGUOUS = "File name '%s' is ambiguous. File with the same name exists on the filesystem.";
private static final String ERROR_ENV_FOUND = "Environment variable '%s' not set.";
private static final String ERROR_OUTPUT_EXISTS = "Output file '%s' already exists.";
private static final String ERROR_INPUT_NOT_EXIST = "File '%s' does not exist.";
private static final String ERROR_CANNOT_CREATE_FILE = "Output file '%s' cannot be created: %s";
public static final String PRFX_ENV = "@ENV:";
public static final String PRFX_FD = "@FD:";
private static EnvironmentVariableResolver envResolver = System::getenv;
public static void setEnvironmentVariableResolver(EnvironmentVariableResolver envResolver) {
if (envResolver == null) {
throw new NullPointerException("Variable envResolver cannot be null.");
}
FileUtil.envResolver = envResolver;
}
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 File getFile(String fileName) {
if (fileName == null) {
throw new NullPointerException("File name cannot be null.");
}
if (fileName.startsWith(PRFX_ENV)) {
if (new File(fileName).exists()) {
throw new SOPGPException.AmbiguousInput(String.format(ERROR_AMBIGUOUS, fileName));
}
String envName = fileName.substring(PRFX_ENV.length());
String envValue = envResolver.resolveEnvironmentVariable(envName);
if (envValue == null) {
throw new IllegalArgumentException(String.format(ERROR_ENV_FOUND, envName));
}
return new File(envValue);
} else if (fileName.startsWith(PRFX_FD)) {
if (new File(fileName).exists()) {
throw new SOPGPException.AmbiguousInput(String.format(ERROR_AMBIGUOUS, fileName));
}
throw new IllegalArgumentException("File descriptors not supported.");
}
return new File(fileName);
}
public static FileInputStream getFileInputStream(String fileName) {
File file = getFile(fileName);
try {
FileInputStream inputStream = new FileInputStream(file);
return inputStream;
} catch (FileNotFoundException e) {
throw new SOPGPException.MissingInput(String.format(ERROR_INPUT_NOT_EXIST, fileName), e);
}
}
public static File createNewFileOrThrow(File file) throws IOException {
if (file == null) {
throw new NullPointerException("File cannot be null.");
}
try {
if (!file.createNewFile()) {
throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_EXISTS, file.getAbsolutePath()));
}
} catch (IOException e) {
throw new IOException(String.format(ERROR_CANNOT_CREATE_FILE, file.getAbsolutePath(), e.getMessage()));
}
return file;
}
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();
}
}
}

View file

@ -10,9 +10,11 @@ public class SOPExecutionExceptionHandler implements CommandLine.IExecutionExcep
@Override @Override
public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) { public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) {
int exitCode = commandLine.getExitCodeExceptionMapper() != null ? int exitCode = commandLine.getExitCodeExceptionMapper() != null ?
commandLine.getExitCodeExceptionMapper().getExitCode(ex) : commandLine.getExitCodeExceptionMapper().getExitCode(ex) :
commandLine.getCommandSpec().exitCodeOnExecutionException(); commandLine.getCommandSpec().exitCodeOnExecutionException();
CommandLine.Help.ColorScheme colorScheme = commandLine.getColorScheme(); CommandLine.Help.ColorScheme colorScheme = commandLine.getColorScheme();
// CHECKSTYLE:OFF // CHECKSTYLE:OFF
if (ex.getMessage() != null) { if (ex.getMessage() != null) {

View file

@ -20,9 +20,13 @@ import sop.cli.picocli.commands.SignCmd;
import sop.cli.picocli.commands.VerifyCmd; import sop.cli.picocli.commands.VerifyCmd;
import sop.cli.picocli.commands.VersionCmd; import sop.cli.picocli.commands.VersionCmd;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
@CommandLine.Command( @CommandLine.Command(
name = "sop", name = "sop",
description = "Stateless OpenPGP Protocol", resourceBundle = "sop",
exitCodeOnInvalidInput = 69, exitCodeOnInvalidInput = 69,
subcommands = { subcommands = {
CommandLine.HelpCommand.class, CommandLine.HelpCommand.class,
@ -39,33 +43,12 @@ import sop.cli.picocli.commands.VersionCmd;
InlineVerifyCmd.class, InlineVerifyCmd.class,
VersionCmd.class, VersionCmd.class,
AutoComplete.GenerateCompletion.class AutoComplete.GenerateCompletion.class
},
exitCodeListHeading = "Exit Codes:%n",
exitCodeList = {
" 0:Successful program execution",
" 1:Generic program error",
" 3:Verification requested but no verifiable signature found",
"13:Unsupported asymmetric algorithm",
"17:Certificate is not encryption capable",
"19:Usage error: Missing argument",
"23:Incomplete verification instructions",
"29:Unable to decrypt",
"31:Password is not human-readable",
"37:Unsupported Option",
"41:Invalid data or data of wrong type encountered",
"53:Non-text input received where text was expected",
"59:Output file already exists",
"61:Input file does not exist",
"67:Key is password protected",
"69:Unsupported subcommand",
"71:Unsupported special prefix (e.g. \"@env/@fd\") of indirect parameter",
"73:Ambiguous input (a filename matching the designator already exists)",
"79:Key is not signing capable"
} }
) )
public class SopCLI { public class SopCLI {
// Singleton // Singleton
static SOP SOP_INSTANCE; static SOP SOP_INSTANCE;
static ResourceBundle cliMsg = ResourceBundle.getBundle("sop");
public static String EXECUTABLE_NAME = "sop"; public static String EXECUTABLE_NAME = "sop";
@ -77,6 +60,13 @@ public class SopCLI {
} }
public static int execute(String[] args) { public static int execute(String[] args) {
// Set locale
new CommandLine(new InitLocale()).parseArgs(args);
cliMsg = ResourceBundle.getBundle("sop");
// Prepare CLI
CommandLine cmd = new CommandLine(SopCLI.class); CommandLine cmd = new CommandLine(SopCLI.class);
// Hide generate-completion command // Hide generate-completion command
CommandLine gen = cmd.getSubcommands().get("generate-completion"); CommandLine gen = cmd.getSubcommands().get("generate-completion");
@ -92,7 +82,8 @@ public class SopCLI {
public static SOP getSop() { public static SOP getSop() {
if (SOP_INSTANCE == null) { if (SOP_INSTANCE == null) {
throw new IllegalStateException("No SOP backend set."); String errorMsg = cliMsg.getString("sop.error.runtime.no_backend_set");
throw new IllegalStateException(errorMsg);
} }
return SOP_INSTANCE; return SOP_INSTANCE;
} }
@ -101,3 +92,18 @@ public class SopCLI {
SOP_INSTANCE = 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
}

View file

@ -5,43 +5,225 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import sop.exception.SOPGPException; 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.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection; import java.util.Collection;
import java.util.Date;
import java.util.Locale;
import java.util.ResourceBundle;
public abstract class AbstractSopCmd implements Runnable { public abstract class AbstractSopCmd implements Runnable {
static final String ERROR_UNSUPPORTED_OPTION = "Option '%s' is not supported."; public interface EnvironmentVariableResolver {
static final String ERROR_FILE_NOT_EXIST = "File '%s' does not exist."; /**
static final String ERROR_OUTPUT_OF_OPTION_EXISTS = "Target %s of option %s already exists."; * 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);
}
void throwIfOutputExists(File outputFile, String optionName) { public static final String PRFX_ENV = "@ENV:";
if (outputFile == null) { 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);
protected final ResourceBundle messages;
protected EnvironmentVariableResolver envResolver = System::getenv;
public AbstractSopCmd() {
this(Locale.getDefault());
}
public AbstractSopCmd(@Nonnull Locale locale) {
messages = ResourceBundle.getBundle("sop", locale);
}
void throwIfOutputExists(String output) {
if (output == null) {
return; return;
} }
File outputFile = new File(output);
if (outputFile.exists()) { if (outputFile.exists()) {
throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_OF_OPTION_EXISTS, outputFile.getAbsolutePath(), optionName)); 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) { void throwIfMissingArg(Object arg, String argName) {
if (arg == null) { if (arg == null) {
throw new SOPGPException.MissingArg(argName + " is required."); String errorMsg = getMsg("sop.error.usage.argument_required", argName);
throw new SOPGPException.MissingArg(errorMsg);
} }
} }
void throwIfEmptyParameters(Collection<?> arg, String parmName) { void throwIfEmptyParameters(Collection<?> arg, String parmName) {
if (arg.isEmpty()) { if (arg.isEmpty()) {
throw new SOPGPException.MissingArg("Parameter '" + parmName + "' is required."); String errorMsg = getMsg("sop.error.usage.parameter_required", parmName);
throw new SOPGPException.MissingArg(errorMsg);
} }
} }
<T> T throwIfUnsupportedSubcommand(T subcommand, String subcommandName) { <T> T throwIfUnsupportedSubcommand(T subcommand, String subcommandName) {
if (subcommand == null) { if (subcommand == null) {
throw new SOPGPException.UnsupportedSubcommand("Command '" + subcommandName + "' is not supported."); String errorMsg = getMsg("sop.error.feature_support.subcommand_not_supported", subcommandName);
throw new SOPGPException.UnsupportedSubcommand(errorMsg);
} }
return subcommand; 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);
}
String errorMsg = getMsg("sop.error.indirect_data_type.designator_fd_not_supported");
throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg);
} 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);
}
if (trimmed.startsWith(PRFX_FD)) {
String errorMsg = getMsg("sop.error.indirect_data_type.designator_fd_not_supported");
throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg);
}
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 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) {
Date date = notAfter.equals("now") ? new Date() : notAfter.equals("-") ? END_OF_TIME : UTCUtil.parseUTCDate(notAfter);
if (date == null) {
String errorMsg = getMsg("sop.error.input.malformed_not_after");
throw new IllegalArgumentException(errorMsg);
}
return date;
}
public Date parseNotBefore(String notBefore) {
Date date = notBefore.equals("now") ? new Date() : notBefore.equals("-") ? BEGINNING_OF_TIME : UTCUtil.parseUTCDate(notBefore);
if (date == null) {
String errorMsg = getMsg("sop.error.input.malformed_not_before");
throw new IllegalArgumentException(errorMsg);
}
return date;
}
} }

View file

@ -4,22 +4,23 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import java.io.IOException;
import picocli.CommandLine; import picocli.CommandLine;
import sop.Ready; import sop.Ready;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI; import sop.cli.picocli.SopCLI;
import sop.enums.ArmorLabel; import sop.enums.ArmorLabel;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.operation.Armor; import sop.operation.Armor;
import java.io.IOException;
@CommandLine.Command(name = "armor", @CommandLine.Command(name = "armor",
description = "Add ASCII Armor to standard input", resourceBundle = "sop",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class ArmorCmd extends AbstractSopCmd { public class ArmorCmd extends AbstractSopCmd {
@CommandLine.Option(names = {"--label"}, description = "Label to be used in the header and tail of the armoring.", paramLabel = "{auto|sig|key|cert|message}") @CommandLine.Option(names = {"--label"},
descriptionKey = "sop.armor.usage.option.label",
paramLabel = "{auto|sig|key|cert|message}")
ArmorLabel label; ArmorLabel label;
@Override @Override
@ -32,8 +33,8 @@ public class ArmorCmd extends AbstractSopCmd {
try { try {
armor.label(label); armor.label(label);
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Armor labels not supported."); String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--label");
System.exit(unsupportedOption.getExitCode()); throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} }
} }
@ -41,13 +42,10 @@ public class ArmorCmd extends AbstractSopCmd {
Ready ready = armor.data(System.in); Ready ready = armor.data(System.in);
ready.writeTo(System.out); ready.writeTo(System.out);
} catch (SOPGPException.BadData badData) { } catch (SOPGPException.BadData badData) {
Print.errln("Bad data."); String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data");
Print.trace(badData); throw new SOPGPException.BadData(errorMsg, badData);
System.exit(badData.getExitCode());
} catch (IOException e) { } catch (IOException e) {
Print.errln("IO Error."); throw new RuntimeException(e);
Print.trace(e);
System.exit(1);
} }
} }
} }

View file

@ -4,16 +4,15 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import java.io.IOException;
import picocli.CommandLine; import picocli.CommandLine;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI; import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.operation.Dearmor; import sop.operation.Dearmor;
import java.io.IOException;
@CommandLine.Command(name = "dearmor", @CommandLine.Command(name = "dearmor",
description = "Remove ASCII Armor from standard input", resourceBundle = "sop",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class DearmorCmd extends AbstractSopCmd { public class DearmorCmd extends AbstractSopCmd {
@ -26,13 +25,10 @@ public class DearmorCmd extends AbstractSopCmd {
dearmor.data(System.in) dearmor.data(System.in)
.writeTo(System.out); .writeTo(System.out);
} catch (SOPGPException.BadData e) { } catch (SOPGPException.BadData e) {
Print.errln("Bad data."); String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data");
Print.trace(e); throw new SOPGPException.BadData(errorMsg, e);
System.exit(e.getExitCode());
} catch (IOException e) { } catch (IOException e) {
Print.errln("IO Error."); throw new RuntimeException(e);
Print.trace(e);
System.exit(1);
} }
} }
} }

View file

@ -4,31 +4,27 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import java.io.File; import picocli.CommandLine;
import java.io.FileInputStream; import sop.DecryptionResult;
import java.io.FileNotFoundException; import sop.ReadyWithResult;
import java.io.FileOutputStream; import sop.SessionKey;
import sop.Verification;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Decrypt;
import sop.util.HexUtil;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import picocli.CommandLine;
import sop.DecryptionResult;
import sop.ReadyWithResult;
import sop.SessionKey;
import sop.Verification;
import sop.cli.picocli.DateParser;
import sop.cli.picocli.FileUtil;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Decrypt;
import sop.util.HexUtil;
@CommandLine.Command(name = "decrypt", @CommandLine.Command(name = "decrypt",
description = "Decrypt a message from standard input", resourceBundle = "sop",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class DecryptCmd extends AbstractSopCmd { public class DecryptCmd extends AbstractSopCmd {
@ -44,54 +40,49 @@ public class DecryptCmd extends AbstractSopCmd {
@CommandLine.Option( @CommandLine.Option(
names = {OPT_SESSION_KEY_OUT}, names = {OPT_SESSION_KEY_OUT},
description = "Can be used to learn the session key on successful decryption", descriptionKey = "sop.decrypt.usage.option.session_key_out",
paramLabel = "SESSIONKEY") paramLabel = "SESSIONKEY")
File sessionKeyOut; String sessionKeyOut;
@CommandLine.Option( @CommandLine.Option(
names = {OPT_WITH_SESSION_KEY}, names = {OPT_WITH_SESSION_KEY},
description = "Provide a session key file. Enables decryption of the \"CIPHERTEXT\" using the session key directly against the \"SEIPD\" packet", descriptionKey = "sop.decrypt.usage.option.with_session_key",
paramLabel = "SESSIONKEY") paramLabel = "SESSIONKEY")
List<String> withSessionKey = new ArrayList<>(); List<String> withSessionKey = new ArrayList<>();
@CommandLine.Option( @CommandLine.Option(
names = {OPT_WITH_PASSWORD}, names = {OPT_WITH_PASSWORD},
description = "Provide a password file. Enables decryption based on any \"SKESK\" packets in the \"CIPHERTEXT\"", descriptionKey = "sop.decrypt.usage.option.with_password",
paramLabel = "PASSWORD") paramLabel = "PASSWORD")
List<String> withPassword = new ArrayList<>(); List<String> withPassword = new ArrayList<>();
@CommandLine.Option(names = {OPT_VERIFY_OUT}, @CommandLine.Option(names = {OPT_VERIFY_OUT},
description = "Produces signature verification status to the designated file", descriptionKey = "sop.decrypt.usage.option.verify_out",
paramLabel = "VERIFICATIONS") paramLabel = "VERIFICATIONS")
File verifyOut; String verifyOut;
@CommandLine.Option(names = {OPT_VERIFY_WITH}, @CommandLine.Option(names = {OPT_VERIFY_WITH},
description = "Certificates whose signatures would be acceptable for signatures over this message", descriptionKey = "sop.decrypt.usage.option.certs",
paramLabel = "CERT") paramLabel = "CERT")
List<File> certs = new ArrayList<>(); List<String> certs = new ArrayList<>();
@CommandLine.Option(names = {OPT_NOT_BEFORE}, @CommandLine.Option(names = {OPT_NOT_BEFORE},
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + descriptionKey = "sop.decrypt.usage.option.not_before",
"Reject signatures with a creation date not in range.\n" +
"Defaults to beginning of time (\"-\").",
paramLabel = "DATE") paramLabel = "DATE")
String notBefore = "-"; String notBefore = "-";
@CommandLine.Option(names = {OPT_NOT_AFTER}, @CommandLine.Option(names = {OPT_NOT_AFTER},
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + descriptionKey = "sop.decrypt.usage.option.not_after",
"Reject signatures with a creation date not in range.\n" +
"Defaults to current system time (\"now\").\n" +
"Accepts special value \"-\" for end of time.",
paramLabel = "DATE") paramLabel = "DATE")
String notAfter = "now"; String notAfter = "now";
@CommandLine.Parameters(index = "0..*", @CommandLine.Parameters(index = "0..*",
description = "Secret keys to attempt decryption with", descriptionKey = "sop.decrypt.usage.param.keys",
paramLabel = "KEY") paramLabel = "KEY")
List<File> keys = new ArrayList<>(); List<String> keys = new ArrayList<>();
@CommandLine.Option(names = {OPT_WITH_KEY_PASSWORD}, @CommandLine.Option(names = {OPT_WITH_KEY_PASSWORD},
description = "Provide indirect file type pointing at passphrase(s) for secret key(s)", descriptionKey = "sop.decrypt.usage.option.with_key_password",
paramLabel = "PASSWORD") paramLabel = "PASSWORD")
List<String> withKeyPassword = new ArrayList<>(); List<String> withKeyPassword = new ArrayList<>();
@ -100,8 +91,8 @@ public class DecryptCmd extends AbstractSopCmd {
Decrypt decrypt = throwIfUnsupportedSubcommand( Decrypt decrypt = throwIfUnsupportedSubcommand(
SopCLI.getSop().decrypt(), "decrypt"); SopCLI.getSop().decrypt(), "decrypt");
throwIfOutputExists(verifyOut, OPT_VERIFY_OUT); throwIfOutputExists(verifyOut);
throwIfOutputExists(sessionKeyOut, OPT_SESSION_KEY_OUT); throwIfOutputExists(sessionKeyOut);
setNotAfter(notAfter, decrypt); setNotAfter(notAfter, decrypt);
setNotBefore(notBefore, decrypt); setNotBefore(notBefore, decrypt);
@ -112,8 +103,8 @@ public class DecryptCmd extends AbstractSopCmd {
setDecryptWith(keys, decrypt); setDecryptWith(keys, decrypt);
if (verifyOut != null && certs.isEmpty()) { if (verifyOut != null && certs.isEmpty()) {
String errorMessage = "Option %s is requested, but no option %s was provided."; String errorMsg = getMsg("sop.error.usage.option_requires_other_option", OPT_VERIFY_OUT, OPT_VERIFY_WITH);
throw new SOPGPException.IncompleteVerification(String.format(errorMessage, OPT_VERIFY_OUT, OPT_VERIFY_WITH)); throw new SOPGPException.IncompleteVerification(errorMsg);
} }
try { try {
@ -122,7 +113,8 @@ public class DecryptCmd extends AbstractSopCmd {
writeSessionKeyOut(result); writeSessionKeyOut(result);
writeVerifyOut(result); writeVerifyOut(result);
} catch (SOPGPException.BadData badData) { } catch (SOPGPException.BadData badData) {
throw new SOPGPException.BadData("No valid OpenPGP message found on Standard Input.", badData); String errorMsg = getMsg("sop.error.input.stdin_not_a_message");
throw new SOPGPException.BadData(errorMsg, badData);
} catch (IOException ioException) { } catch (IOException ioException) {
throw new RuntimeException(ioException); throw new RuntimeException(ioException);
} }
@ -130,9 +122,8 @@ public class DecryptCmd extends AbstractSopCmd {
private void writeVerifyOut(DecryptionResult result) throws IOException { private void writeVerifyOut(DecryptionResult result) throws IOException {
if (verifyOut != null) { if (verifyOut != null) {
FileUtil.createNewFileOrThrow(verifyOut); try (OutputStream fileOut = getOutput(verifyOut)) {
try (FileOutputStream outputStream = new FileOutputStream(verifyOut)) { PrintWriter writer = new PrintWriter(fileOut);
PrintWriter writer = new PrintWriter(outputStream);
for (Verification verification : result.getVerifications()) { for (Verification verification : result.getVerifications()) {
// CHECKSTYLE:OFF // CHECKSTYLE:OFF
writer.println(verification.toString()); writer.println(verification.toString());
@ -145,11 +136,9 @@ public class DecryptCmd extends AbstractSopCmd {
private void writeSessionKeyOut(DecryptionResult result) throws IOException { private void writeSessionKeyOut(DecryptionResult result) throws IOException {
if (sessionKeyOut != null) { if (sessionKeyOut != null) {
FileUtil.createNewFileOrThrow(sessionKeyOut); try (OutputStream outputStream = getOutput(sessionKeyOut)) {
try (FileOutputStream outputStream = new FileOutputStream(sessionKeyOut)) {
if (!result.getSessionKey().isPresent()) { if (!result.getSessionKey().isPresent()) {
String errorMsg = "Session key not extracted. Possibly the feature %s is not supported."; String errorMsg = getMsg("sop.error.runtime.no_session_key_extracted");
throw new SOPGPException.UnsupportedOption(String.format(errorMsg, OPT_SESSION_KEY_OUT)); throw new SOPGPException.UnsupportedOption(String.format(errorMsg, OPT_SESSION_KEY_OUT));
} else { } else {
SessionKey sessionKey = result.getSessionKey().get(); SessionKey sessionKey = result.getSessionKey().get();
@ -160,30 +149,29 @@ public class DecryptCmd extends AbstractSopCmd {
} }
} }
private void setDecryptWith(List<File> keys, Decrypt decrypt) { private void setDecryptWith(List<String> keys, Decrypt decrypt) {
for (File key : keys) { for (String key : keys) {
try (FileInputStream keyIn = new FileInputStream(key)) { try (InputStream keyIn = getInput(key)) {
decrypt.withKey(keyIn); decrypt.withKey(keyIn);
} catch (SOPGPException.KeyIsProtected keyIsProtected) { } catch (SOPGPException.KeyIsProtected keyIsProtected) {
throw new SOPGPException.KeyIsProtected("Key in file " + key.getAbsolutePath() + " is password protected.", keyIsProtected); String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", key);
throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected);
} catch (SOPGPException.BadData badData) { } catch (SOPGPException.BadData badData) {
throw new SOPGPException.BadData("File " + key.getAbsolutePath() + " does not contain a private key.", badData); String errorMsg = getMsg("sop.error.input.not_a_private_key", key);
} catch (FileNotFoundException e) { throw new SOPGPException.BadData(errorMsg, badData);
throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, key.getAbsolutePath()), e);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
} }
private void setVerifyWith(List<File> certs, Decrypt decrypt) { private void setVerifyWith(List<String> certs, Decrypt decrypt) {
for (File cert : certs) { for (String cert : certs) {
try (FileInputStream certIn = new FileInputStream(cert)) { try (InputStream certIn = getInput(cert)) {
decrypt.verifyWithCert(certIn); decrypt.verifyWithCert(certIn);
} catch (FileNotFoundException e) {
throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, cert.getAbsolutePath()), e);
} catch (SOPGPException.BadData badData) { } catch (SOPGPException.BadData badData) {
throw new SOPGPException.BadData("File " + cert.getAbsolutePath() + " does not contain a valid certificate.", badData); String errorMsg = getMsg("sop.error.input.not_a_certificate", cert);
throw new SOPGPException.BadData(errorMsg, badData);
} catch (IOException ioException) { } catch (IOException ioException) {
throw new RuntimeException(ioException); throw new RuntimeException(ioException);
} }
@ -195,12 +183,13 @@ public class DecryptCmd extends AbstractSopCmd {
for (String sessionKeyFile : withSessionKey) { for (String sessionKeyFile : withSessionKey) {
String sessionKey; String sessionKey;
try { try {
sessionKey = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(sessionKeyFile)); sessionKey = stringFromInputStream(getInput(sessionKeyFile));
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
if (!sessionKeyPattern.matcher(sessionKey).matches()) { if (!sessionKeyPattern.matcher(sessionKey).matches()) {
throw new IllegalArgumentException("Session keys are expected in the format 'ALGONUM:HEXKEY'."); String errorMsg = getMsg("sop.error.input.malformed_session_key");
throw new IllegalArgumentException(errorMsg);
} }
String[] split = sessionKey.split(":"); String[] split = sessionKey.split(":");
byte algorithm = (byte) Integer.parseInt(split[0]); byte algorithm = (byte) Integer.parseInt(split[0]);
@ -209,7 +198,8 @@ public class DecryptCmd extends AbstractSopCmd {
try { try {
decrypt.withSessionKey(new SessionKey(algorithm, key)); decrypt.withSessionKey(new SessionKey(algorithm, key));
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, OPT_WITH_SESSION_KEY), unsupportedOption); String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_SESSION_KEY);
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} }
} }
} }
@ -217,10 +207,12 @@ public class DecryptCmd extends AbstractSopCmd {
private void setWithPasswords(List<String> withPassword, Decrypt decrypt) { private void setWithPasswords(List<String> withPassword, Decrypt decrypt) {
for (String passwordFile : withPassword) { for (String passwordFile : withPassword) {
try { try {
String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(passwordFile)); String password = stringFromInputStream(getInput(passwordFile));
decrypt.withPassword(password); decrypt.withPassword(password);
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, OPT_WITH_PASSWORD), unsupportedOption);
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_PASSWORD);
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -230,10 +222,12 @@ public class DecryptCmd extends AbstractSopCmd {
private void setWithKeyPassword(List<String> withKeyPassword, Decrypt decrypt) { private void setWithKeyPassword(List<String> withKeyPassword, Decrypt decrypt) {
for (String passwordFile : withKeyPassword) { for (String passwordFile : withKeyPassword) {
try { try {
String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(passwordFile)); String password = stringFromInputStream(getInput(passwordFile));
decrypt.withKeyPassword(password); decrypt.withKeyPassword(password);
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, OPT_WITH_KEY_PASSWORD), unsupportedOption);
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_KEY_PASSWORD);
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -241,20 +235,22 @@ public class DecryptCmd extends AbstractSopCmd {
} }
private void setNotAfter(String notAfter, Decrypt decrypt) { private void setNotAfter(String notAfter, Decrypt decrypt) {
Date notAfterDate = DateParser.parseNotAfter(notAfter); Date notAfterDate = parseNotAfter(notAfter);
try { try {
decrypt.verifyNotAfter(notAfterDate); decrypt.verifyNotAfter(notAfterDate);
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, OPT_NOT_AFTER), 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) { private void setNotBefore(String notBefore, Decrypt decrypt) {
Date notBeforeDate = DateParser.parseNotBefore(notBefore); Date notBeforeDate = parseNotBefore(notBefore);
try { try {
decrypt.verifyNotBefore(notBeforeDate); decrypt.verifyNotBefore(notBeforeDate);
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, OPT_NOT_BEFORE), unsupportedOption); String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_BEFORE);
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} }
} }
} }

View file

@ -4,55 +4,52 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import picocli.CommandLine; import picocli.CommandLine;
import sop.Ready; import sop.Ready;
import sop.cli.picocli.FileUtil;
import sop.cli.picocli.SopCLI; import sop.cli.picocli.SopCLI;
import sop.enums.EncryptAs; import sop.enums.EncryptAs;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.operation.Encrypt; import sop.operation.Encrypt;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@CommandLine.Command(name = "encrypt", @CommandLine.Command(name = "encrypt",
description = "Encrypt a message from standard input", resourceBundle = "sop",
exitCodeOnInvalidInput = 37) exitCodeOnInvalidInput = 37)
public class EncryptCmd extends AbstractSopCmd { public class EncryptCmd extends AbstractSopCmd {
@CommandLine.Option(names = "--no-armor", @CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output", descriptionKey = "sop.encrypt.usage.option.armor",
negatable = true) negatable = true)
boolean armor = true; boolean armor = true;
@CommandLine.Option(names = {"--as"}, @CommandLine.Option(names = {"--as"},
description = "Type of the input data. Defaults to 'binary'", descriptionKey = "sop.encrypt.usage.option.type",
paramLabel = "{binary|text}") paramLabel = "{binary|text}")
EncryptAs type; EncryptAs type;
@CommandLine.Option(names = "--with-password", @CommandLine.Option(names = "--with-password",
description = "Encrypt the message with a password provided by the given password file", descriptionKey = "sop.encrypt.usage.option.with_password",
paramLabel = "PASSWORD") paramLabel = "PASSWORD")
List<String> withPassword = new ArrayList<>(); List<String> withPassword = new ArrayList<>();
@CommandLine.Option(names = "--sign-with", @CommandLine.Option(names = "--sign-with",
description = "Sign the output with a private key", descriptionKey = "sop.encrypt.usage.option.sign_with",
paramLabel = "KEY") paramLabel = "KEY")
List<File> signWith = new ArrayList<>(); List<String> signWith = new ArrayList<>();
@CommandLine.Option(names = "--with-key-password", @CommandLine.Option(names = "--with-key-password",
description = "Provide indirect file type pointing at passphrase(s) for secret key(s)", descriptionKey = "sop.encrypt.usage.option.with_key_password",
paramLabel = "PASSWORD") paramLabel = "PASSWORD")
List<String> withKeyPassword = new ArrayList<>(); List<String> withKeyPassword = new ArrayList<>();
@CommandLine.Parameters(description = "Certificates the message gets encrypted to", @CommandLine.Parameters(descriptionKey = "sop.encrypt.usage.param.certs",
index = "0..*", index = "0..*",
paramLabel = "CERTS") paramLabel = "CERTS")
List<File> certs = new ArrayList<>(); List<String> certs = new ArrayList<>();
@Override @Override
public void run() { public void run() {
@ -63,20 +60,24 @@ public class EncryptCmd extends AbstractSopCmd {
try { try {
encrypt.mode(type); encrypt.mode(type);
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption("Unsupported option '--as'.", unsupportedOption); String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} }
} }
if (withPassword.isEmpty() && certs.isEmpty()) { if (withPassword.isEmpty() && certs.isEmpty()) {
throw new SOPGPException.MissingArg("At least one password file or cert file required for encryption."); String errorMsg = getMsg("sop.error.usage.password_or_cert_required");
throw new SOPGPException.MissingArg(errorMsg);
} }
for (String passwordFileName : withPassword) { for (String passwordFileName : withPassword) {
try { try {
String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(passwordFileName)); String password = stringFromInputStream(getInput(passwordFileName));
encrypt.withPassword(password); encrypt.withPassword(password);
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption("Unsupported option '--with-password'.", unsupportedOption);
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-password");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -84,46 +85,51 @@ public class EncryptCmd extends AbstractSopCmd {
for (String passwordFileName : withKeyPassword) { for (String passwordFileName : withKeyPassword) {
try { try {
String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(passwordFileName)); String password = stringFromInputStream(getInput(passwordFileName));
encrypt.withKeyPassword(password); encrypt.withKeyPassword(password);
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption("Unsupported option '--with-key-password'.", unsupportedOption);
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
for (File keyFile : signWith) { for (String keyInput : signWith) {
try (FileInputStream keyIn = new FileInputStream(keyFile)) { try (InputStream keyIn = getInput(keyInput)) {
encrypt.signWith(keyIn); encrypt.signWith(keyIn);
} catch (FileNotFoundException e) {
throw new SOPGPException.MissingInput("Key file " + keyFile.getAbsolutePath() + " not found.", e);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} catch (SOPGPException.KeyIsProtected keyIsProtected) { } catch (SOPGPException.KeyIsProtected keyIsProtected) {
throw new SOPGPException.KeyIsProtected("Key from " + keyFile.getAbsolutePath() + " is password protected.", keyIsProtected); String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput);
throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected);
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
throw new SOPGPException.UnsupportedAsymmetricAlgo("Key from " + keyFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo); String errorMsg = getMsg("sop.error.runtime.key_uses_unsupported_asymmetric_algorithm", keyInput);
throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo);
} catch (SOPGPException.KeyCannotSign keyCannotSign) { } catch (SOPGPException.KeyCannotSign keyCannotSign) {
throw new SOPGPException.KeyCannotSign("Key from " + keyFile.getAbsolutePath() + " cannot sign.", keyCannotSign); String errorMsg = getMsg("sop.error.runtime.key_cannot_sign", keyInput);
throw new SOPGPException.KeyCannotSign(errorMsg, keyCannotSign);
} catch (SOPGPException.BadData badData) { } catch (SOPGPException.BadData badData) {
throw new SOPGPException.BadData("Key file " + keyFile.getAbsolutePath() + " does not contain a valid OpenPGP private key.", badData); String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput);
throw new SOPGPException.BadData(errorMsg, badData);
} }
} }
for (File certFile : certs) { for (String certInput : certs) {
try (FileInputStream certIn = new FileInputStream(certFile)) { try (InputStream certIn = getInput(certInput)) {
encrypt.withCert(certIn); encrypt.withCert(certIn);
} catch (FileNotFoundException e) {
throw new SOPGPException.MissingInput("Certificate file " + certFile.getAbsolutePath() + " not found.", e);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
throw new SOPGPException.UnsupportedAsymmetricAlgo("Certificate from " + certFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo); String errorMsg = getMsg("sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput);
throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo);
} catch (SOPGPException.CertCannotEncrypt certCannotEncrypt) { } catch (SOPGPException.CertCannotEncrypt certCannotEncrypt) {
throw new SOPGPException.CertCannotEncrypt("Certificate from " + certFile.getAbsolutePath() + " is not capable of encryption.", certCannotEncrypt); String errorMsg = getMsg("sop.error.runtime.cert_cannot_encrypt", certInput);
throw new SOPGPException.CertCannotEncrypt(errorMsg, certCannotEncrypt);
} catch (SOPGPException.BadData badData) { } catch (SOPGPException.BadData badData) {
throw new SOPGPException.BadData("Certificate file " + certFile.getAbsolutePath() + " does not contain a valid OpenPGP certificate.", badData); String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput);
throw new SOPGPException.BadData(errorMsg, badData);
} }
} }

View file

@ -13,12 +13,12 @@ import sop.exception.SOPGPException;
import sop.operation.ExtractCert; import sop.operation.ExtractCert;
@CommandLine.Command(name = "extract-cert", @CommandLine.Command(name = "extract-cert",
description = "Extract a public key certificate from a secret key from standard input", resourceBundle = "sop",
exitCodeOnInvalidInput = 37) exitCodeOnInvalidInput = 37)
public class ExtractCertCmd extends AbstractSopCmd { public class ExtractCertCmd extends AbstractSopCmd {
@CommandLine.Option(names = "--no-armor", @CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output", descriptionKey = "sop.extract-cert.usage.option.armor",
negatable = true) negatable = true)
boolean armor = true; boolean armor = true;
@ -37,7 +37,8 @@ public class ExtractCertCmd extends AbstractSopCmd {
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} catch (SOPGPException.BadData badData) { } catch (SOPGPException.BadData badData) {
throw new SOPGPException.BadData("Standard Input does not contain valid OpenPGP private key material.", badData); String errorMsg = getMsg("sop.error.input.stdin_not_a_private_key");
throw new SOPGPException.BadData(errorMsg, badData);
} }
} }
} }

View file

@ -4,33 +4,31 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import picocli.CommandLine; import picocli.CommandLine;
import sop.Ready; import sop.Ready;
import sop.cli.picocli.FileUtil;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI; import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.operation.GenerateKey; import sop.operation.GenerateKey;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@CommandLine.Command(name = "generate-key", @CommandLine.Command(name = "generate-key",
description = "Generate a secret key", resourceBundle = "sop",
exitCodeOnInvalidInput = 37) exitCodeOnInvalidInput = 37)
public class GenerateKeyCmd extends AbstractSopCmd { public class GenerateKeyCmd extends AbstractSopCmd {
@CommandLine.Option(names = "--no-armor", @CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output", descriptionKey = "sop.generate-key.usage.option.armor",
negatable = true) negatable = true)
boolean armor = true; boolean armor = true;
@CommandLine.Parameters(description = "User-ID, eg. \"Alice <alice@example.com>\"") @CommandLine.Parameters(descriptionKey = "sop.generate-key.usage.option.user_id")
List<String> userId = new ArrayList<>(); List<String> userId = new ArrayList<>();
@CommandLine.Option(names = "--with-key-password", @CommandLine.Option(names = "--with-key-password",
description = "Indirect file type pointing to file containing password to protect the key", descriptionKey = "sop.generate-key.usage.option.with_key_password",
paramLabel = "PASSWORD") paramLabel = "PASSWORD")
String withKeyPassword; String withKeyPassword;
@ -49,10 +47,11 @@ public class GenerateKeyCmd extends AbstractSopCmd {
if (withKeyPassword != null) { if (withKeyPassword != null) {
try { try {
String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(withKeyPassword)); String password = stringFromInputStream(getInput(withKeyPassword));
generateKey.withKeyPassword(password); generateKey.withKeyPassword(password);
} catch (SOPGPException.UnsupportedOption e) { } catch (SOPGPException.UnsupportedOption e) {
throw new SOPGPException.UnsupportedOption("Option '--with-key-password' is not supported."); String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password");
throw new SOPGPException.UnsupportedOption(errorMsg, e);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -61,18 +60,8 @@ public class GenerateKeyCmd extends AbstractSopCmd {
try { try {
Ready ready = generateKey.generate(); Ready ready = generateKey.generate();
ready.writeTo(System.out); ready.writeTo(System.out);
} catch (SOPGPException.MissingArg missingArg) {
Print.errln("Missing argument.");
Print.trace(missingArg);
System.exit(missingArg.getExitCode());
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
Print.errln("Unsupported asymmetric algorithm.");
Print.trace(unsupportedAsymmetricAlgo);
System.exit(unsupportedAsymmetricAlgo.getExitCode());
} catch (IOException e) { } catch (IOException e) {
Print.errln("IO Error."); throw new RuntimeException(e);
Print.trace(e);
System.exit(1);
} }
} }
} }

View file

@ -4,29 +4,28 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import picocli.CommandLine; import picocli.CommandLine;
import sop.Signatures; import sop.Signatures;
import sop.cli.picocli.SopCLI; import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.operation.InlineDetach; import sop.operation.InlineDetach;
import java.io.IOException;
import java.io.OutputStream;
@CommandLine.Command(name = "inline-detach", @CommandLine.Command(name = "inline-detach",
description = "Split a clearsigned message", resourceBundle = "sop",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class InlineDetachCmd extends AbstractSopCmd { public class InlineDetachCmd extends AbstractSopCmd {
@CommandLine.Option( @CommandLine.Option(
names = {"--signatures-out"}, names = {"--signatures-out"},
description = "Destination to which a detached signatures block will be written", descriptionKey = "sop.inline-detach.usage.option.signatures_out",
paramLabel = "SIGNATURES") paramLabel = "SIGNATURES")
File signaturesOut; String signaturesOut;
@CommandLine.Option(names = "--no-armor", @CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output", descriptionKey = "sop.inline-detach.usage.option.armor",
negatable = true) negatable = true)
boolean armor = true; boolean armor = true;
@ -35,18 +34,17 @@ public class InlineDetachCmd extends AbstractSopCmd {
InlineDetach inlineDetach = throwIfUnsupportedSubcommand( InlineDetach inlineDetach = throwIfUnsupportedSubcommand(
SopCLI.getSop().inlineDetach(), "inline-detach"); SopCLI.getSop().inlineDetach(), "inline-detach");
throwIfOutputExists(signaturesOut, "--signatures-out"); throwIfOutputExists(signaturesOut);
throwIfMissingArg(signaturesOut, "--signatures-out"); throwIfMissingArg(signaturesOut, "--signatures-out");
if (!armor) { if (!armor) {
inlineDetach.noArmor(); inlineDetach.noArmor();
} }
try { try (OutputStream outputStream = getOutput(signaturesOut)) {
Signatures signatures = inlineDetach Signatures signatures = inlineDetach
.message(System.in).writeTo(System.out); .message(System.in).writeTo(System.out);
signaturesOut.createNewFile(); signatures.writeTo(outputStream);
signatures.writeTo(new FileOutputStream(signaturesOut));
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View file

@ -8,99 +8,90 @@ import picocli.CommandLine;
import sop.MicAlg; import sop.MicAlg;
import sop.ReadyWithResult; import sop.ReadyWithResult;
import sop.SigningResult; import sop.SigningResult;
import sop.cli.picocli.FileUtil;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI; import sop.cli.picocli.SopCLI;
import sop.enums.InlineSignAs; import sop.enums.InlineSignAs;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.operation.InlineSign; import sop.operation.InlineSign;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@CommandLine.Command(name = "inline-sign", @CommandLine.Command(name = "inline-sign",
description = "Create an inline-signed message from data on standard input", resourceBundle = "sop",
exitCodeOnInvalidInput = 37) exitCodeOnInvalidInput = 37)
public class InlineSignCmd extends AbstractSopCmd { public class InlineSignCmd extends AbstractSopCmd {
@CommandLine.Option(names = "--no-armor", @CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output", descriptionKey = "sop.inline-sign.usage.option.armor",
negatable = true) negatable = true)
boolean armor = true; boolean armor = true;
@CommandLine.Option(names = "--as", description = "Defaults to 'binary'. If '--as=text' and the input data is not valid UTF-8, inline-sign fails with return code 53.", @CommandLine.Option(names = "--as",
descriptionKey = "sop.inline-sign.usage.option.as",
paramLabel = "{binary|text|cleartextsigned}") paramLabel = "{binary|text|cleartextsigned}")
InlineSignAs type; InlineSignAs type;
@CommandLine.Parameters(description = "Secret keys used for signing", @CommandLine.Parameters(descriptionKey = "sop.inline-sign.usage.parameter.keys",
paramLabel = "KEYS") paramLabel = "KEYS")
List<File> secretKeyFile = new ArrayList<>(); List<String> secretKeyFile = new ArrayList<>();
@CommandLine.Option(names = "--with-key-password", description = "Password(s) to unlock the secret key(s) with", @CommandLine.Option(names = "--with-key-password",
descriptionKey = "sop.inline-sign.usage.option.with_key_password",
paramLabel = "PASSWORD") paramLabel = "PASSWORD")
List<String> withKeyPassword = new ArrayList<>(); List<String> withKeyPassword = new ArrayList<>();
@CommandLine.Option(names = "--micalg-out", description = "Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156)", @CommandLine.Option(names = "--micalg-out",
descriptionKey = "sop.inline-sign.usage.option.micalg",
paramLabel = "MICALG") paramLabel = "MICALG")
File micAlgOut; String micAlgOut;
@Override @Override
public void run() { public void run() {
InlineSign inlineSign = throwIfUnsupportedSubcommand( InlineSign inlineSign = throwIfUnsupportedSubcommand(
SopCLI.getSop().inlineSign(), "inline-sign"); SopCLI.getSop().inlineSign(), "inline-sign");
throwIfOutputExists(micAlgOut, "--micalg-out"); throwIfOutputExists(micAlgOut);
if (type != null) { if (type != null) {
try { try {
inlineSign.mode(type); inlineSign.mode(type);
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--as'"); String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as");
Print.trace(unsupportedOption); throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
System.exit(unsupportedOption.getExitCode());
} }
} }
if (secretKeyFile.isEmpty()) { if (secretKeyFile.isEmpty()) {
Print.errln("Missing required parameter 'KEYS'."); String errorMsg = getMsg("sop.error.usage.parameter_required", "KEYS");
System.exit(19); throw new SOPGPException.MissingArg(errorMsg);
} }
for (String passwordFile : withKeyPassword) { for (String passwordFile : withKeyPassword) {
try { try {
String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(passwordFile)); String password = stringFromInputStream(getInput(passwordFile));
inlineSign.withKeyPassword(password); inlineSign.withKeyPassword(password);
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-key-password"), unsupportedOption); String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
for (File keyFile : secretKeyFile) { for (String keyInput : secretKeyFile) {
try (FileInputStream keyIn = new FileInputStream(keyFile)) { try (InputStream keyIn = getInput(keyInput)) {
inlineSign.key(keyIn); inlineSign.key(keyIn);
} catch (FileNotFoundException e) {
Print.errln("File " + keyFile.getAbsolutePath() + " does not exist.");
Print.trace(e);
System.exit(1);
} catch (IOException e) { } catch (IOException e) {
Print.errln("Cannot access file " + keyFile.getAbsolutePath()); throw new RuntimeException(e);
Print.trace(e);
System.exit(1);
} catch (SOPGPException.KeyIsProtected e) { } catch (SOPGPException.KeyIsProtected e) {
Print.errln("Key " + keyFile.getName() + " is password protected."); String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput);
Print.trace(e); throw new SOPGPException.KeyIsProtected(errorMsg, e);
System.exit(1);
} catch (SOPGPException.BadData badData) { } catch (SOPGPException.BadData badData) {
Print.errln("Bad data in key file " + keyFile.getAbsolutePath() + ":"); String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput);
Print.trace(badData); throw new SOPGPException.BadData(errorMsg, badData);
System.exit(badData.getExitCode());
} }
} }
@ -115,19 +106,12 @@ public class InlineSignCmd extends AbstractSopCmd {
MicAlg micAlg = result.getMicAlg(); MicAlg micAlg = result.getMicAlg();
if (micAlgOut != null) { if (micAlgOut != null) {
// Write micalg out // Write micalg out
micAlgOut.createNewFile(); OutputStream micAlgOutStream = getOutput(micAlgOut);
FileOutputStream micAlgOutStream = new FileOutputStream(micAlgOut);
micAlg.writeTo(micAlgOutStream); micAlg.writeTo(micAlgOutStream);
micAlgOutStream.close(); micAlgOutStream.close();
} }
} catch (IOException e) { } catch (IOException e) {
Print.errln("IO Error."); throw new RuntimeException(e);
Print.trace(e);
System.exit(1);
} catch (SOPGPException.ExpectedText expectedText) {
Print.errln("Expected text input, but got binary data.");
Print.trace(expectedText);
System.exit(expectedText.getExitCode());
} }
} }
} }

View file

@ -7,91 +7,76 @@ package sop.cli.picocli.commands;
import picocli.CommandLine; import picocli.CommandLine;
import sop.ReadyWithResult; import sop.ReadyWithResult;
import sop.Verification; import sop.Verification;
import sop.cli.picocli.DateParser;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI; import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.operation.InlineVerify; import sop.operation.InlineVerify;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@CommandLine.Command(name = "inline-verify", @CommandLine.Command(name = "inline-verify",
description = "Verify inline-signed data from standard input", resourceBundle = "sop",
exitCodeOnInvalidInput = 37) exitCodeOnInvalidInput = 37)
public class InlineVerifyCmd extends AbstractSopCmd { public class InlineVerifyCmd extends AbstractSopCmd {
@CommandLine.Parameters(arity = "1..*", @CommandLine.Parameters(arity = "1..*",
description = "Public key certificates", descriptionKey = "sop.inline-verify.usage.parameter.certs",
paramLabel = "CERT") paramLabel = "CERT")
List<File> certificates = new ArrayList<>(); List<String> certificates = new ArrayList<>();
@CommandLine.Option(names = {"--not-before"}, @CommandLine.Option(names = {"--not-before"},
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + descriptionKey = "sop.inline-verify.usage.option.not_before",
"Reject signatures with a creation date not in range.\n" +
"Defaults to beginning of time (\"-\").",
paramLabel = "DATE") paramLabel = "DATE")
String notBefore = "-"; String notBefore = "-";
@CommandLine.Option(names = {"--not-after"}, @CommandLine.Option(names = {"--not-after"},
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + descriptionKey = "sop.inline-verify.usage.option.not_after",
"Reject signatures with a creation date not in range.\n" +
"Defaults to current system time (\"now\").\n" +
"Accepts special value \"-\" for end of time.",
paramLabel = "DATE") paramLabel = "DATE")
String notAfter = "now"; String notAfter = "now";
@CommandLine.Option(names = "--verifications-out", @CommandLine.Option(names = "--verifications-out",
description = "File to write details over successful verifications to") descriptionKey = "sop.inline-verify.usage.option.verifications_out")
File verificationsOut; String verificationsOut;
@Override @Override
public void run() { public void run() {
InlineVerify inlineVerify = throwIfUnsupportedSubcommand( InlineVerify inlineVerify = throwIfUnsupportedSubcommand(
SopCLI.getSop().inlineVerify(), "inline-verify"); SopCLI.getSop().inlineVerify(), "inline-verify");
throwIfOutputExists(verificationsOut, "--verifications-out"); throwIfOutputExists(verificationsOut);
if (notAfter != null) { if (notAfter != null) {
try { try {
inlineVerify.notAfter(DateParser.parseNotAfter(notAfter)); inlineVerify.notAfter(parseNotAfter(notAfter));
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--not-after'."); String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after");
Print.trace(unsupportedOption); throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
System.exit(unsupportedOption.getExitCode());
} }
} }
if (notBefore != null) { if (notBefore != null) {
try { try {
inlineVerify.notBefore(DateParser.parseNotBefore(notBefore)); inlineVerify.notBefore(parseNotBefore(notBefore));
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--not-before'."); String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before");
Print.trace(unsupportedOption); throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
System.exit(unsupportedOption.getExitCode());
} }
} }
for (File certFile : certificates) { for (String certInput : certificates) {
try (FileInputStream certIn = new FileInputStream(certFile)) { try (InputStream certIn = getInput(certInput)) {
inlineVerify.cert(certIn); inlineVerify.cert(certIn);
} catch (FileNotFoundException fileNotFoundException) {
Print.errln("Certificate file " + certFile.getAbsolutePath() + " not found.");
Print.trace(fileNotFoundException);
System.exit(1);
} catch (IOException ioException) { } catch (IOException ioException) {
Print.errln("IO Error."); throw new RuntimeException(ioException);
Print.trace(ioException); } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
System.exit(1); String errorMsg = getMsg("sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput);
throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo);
} catch (SOPGPException.BadData badData) { } catch (SOPGPException.BadData badData) {
Print.errln("Certificate file " + certFile.getAbsolutePath() + " appears to not contain a valid OpenPGP certificate."); String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput);
Print.trace(badData); throw new SOPGPException.BadData(errorMsg, badData);
System.exit(badData.getExitCode());
} }
} }
@ -100,23 +85,18 @@ public class InlineVerifyCmd extends AbstractSopCmd {
ReadyWithResult<List<Verification>> ready = inlineVerify.data(System.in); ReadyWithResult<List<Verification>> ready = inlineVerify.data(System.in);
verifications = ready.writeTo(System.out); verifications = ready.writeTo(System.out);
} catch (SOPGPException.NoSignature e) { } catch (SOPGPException.NoSignature e) {
Print.errln("No verifiable signature found."); String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found");
Print.trace(e); throw new SOPGPException.NoSignature(errorMsg, e);
System.exit(e.getExitCode());
} catch (IOException ioException) { } catch (IOException ioException) {
Print.errln("IO Error."); throw new RuntimeException(ioException);
Print.trace(ioException);
System.exit(1);
} catch (SOPGPException.BadData badData) { } catch (SOPGPException.BadData badData) {
Print.errln("Standard Input appears not to contain a valid OpenPGP message."); String errorMsg = getMsg("sop.error.input.stdin_not_a_message");
Print.trace(badData); throw new SOPGPException.BadData(errorMsg, badData);
System.exit(badData.getExitCode());
} }
if (verificationsOut != null) { if (verificationsOut != null) {
try { try (OutputStream outputStream = getOutput(verificationsOut)) {
verificationsOut.createNewFile(); PrintWriter pw = new PrintWriter(outputStream);
PrintWriter pw = new PrintWriter(verificationsOut);
for (Verification verification : verifications) { for (Verification verification : verifications) {
// CHECKSTYLE:OFF // CHECKSTYLE:OFF
pw.println(verification); pw.println(verification);

View file

@ -4,126 +4,110 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import picocli.CommandLine; import picocli.CommandLine;
import sop.MicAlg; import sop.MicAlg;
import sop.ReadyWithResult; import sop.ReadyWithResult;
import sop.SigningResult; import sop.SigningResult;
import sop.cli.picocli.FileUtil;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI; import sop.cli.picocli.SopCLI;
import sop.enums.SignAs; import sop.enums.SignAs;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.operation.Sign; 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", @CommandLine.Command(name = "sign",
description = "Create a detached signature on the data from standard input", resourceBundle = "sop",
exitCodeOnInvalidInput = 37) exitCodeOnInvalidInput = 37)
public class SignCmd extends AbstractSopCmd { public class SignCmd extends AbstractSopCmd {
@CommandLine.Option(names = "--no-armor", @CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output", descriptionKey = "sop.sign.usage.option.armor",
negatable = true) negatable = true)
boolean armor = true; boolean armor = true;
@CommandLine.Option(names = "--as", description = "Defaults to 'binary'. If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53.", @CommandLine.Option(names = "--as",
descriptionKey = "sop.sign.usage.option.as",
paramLabel = "{binary|text}") paramLabel = "{binary|text}")
SignAs type; SignAs type;
@CommandLine.Parameters(description = "Secret keys used for signing", @CommandLine.Parameters(descriptionKey = "sop.sign.usage.parameter.keys",
paramLabel = "KEYS") paramLabel = "KEYS")
List<File> secretKeyFile = new ArrayList<>(); List<String> secretKeyFile = new ArrayList<>();
@CommandLine.Option(names = "--with-key-password", description = "Password(s) to unlock the secret key(s) with", @CommandLine.Option(names = "--with-key-password",
descriptionKey = "sop.sign.usage.option.with_key_password",
paramLabel = "PASSWORD") paramLabel = "PASSWORD")
List<String> withKeyPassword = new ArrayList<>(); List<String> withKeyPassword = new ArrayList<>();
@CommandLine.Option(names = "--micalg-out", description = "Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156)", @CommandLine.Option(names = "--micalg-out",
descriptionKey = "sop.sign.usage.option.micalg_out",
paramLabel = "MICALG") paramLabel = "MICALG")
File micAlgOut; String micAlgOut;
@Override @Override
public void run() { public void run() {
Sign sign = throwIfUnsupportedSubcommand( DetachedSign detachedSign = throwIfUnsupportedSubcommand(
SopCLI.getSop().sign(), "sign"); SopCLI.getSop().detachedSign(), "sign");
throwIfOutputExists(micAlgOut, "--micalg-out"); throwIfOutputExists(micAlgOut);
throwIfEmptyParameters(secretKeyFile, "KEYS"); throwIfEmptyParameters(secretKeyFile, "KEYS");
if (type != null) { if (type != null) {
try { try {
sign.mode(type); detachedSign.mode(type);
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--as'"); String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as");
Print.trace(unsupportedOption); throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
System.exit(unsupportedOption.getExitCode());
} }
} }
for (String passwordFile : withKeyPassword) { for (String passwordFile : withKeyPassword) {
try { try {
String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(passwordFile)); String password = stringFromInputStream(getInput(passwordFile));
sign.withKeyPassword(password); detachedSign.withKeyPassword(password);
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-key-password"), unsupportedOption); String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
for (File keyFile : secretKeyFile) { for (String keyInput : secretKeyFile) {
try (FileInputStream keyIn = new FileInputStream(keyFile)) { try (InputStream keyIn = getInput(keyInput)) {
sign.key(keyIn); detachedSign.key(keyIn);
} catch (FileNotFoundException e) {
Print.errln("File " + keyFile.getAbsolutePath() + " does not exist.");
Print.trace(e);
System.exit(1);
} catch (IOException e) { } catch (IOException e) {
Print.errln("Cannot access file " + keyFile.getAbsolutePath()); throw new RuntimeException(e);
Print.trace(e); } catch (SOPGPException.KeyIsProtected keyIsProtected) {
System.exit(1); String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput);
} catch (SOPGPException.KeyIsProtected e) { throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected);
Print.errln("Key " + keyFile.getName() + " is password protected.");
Print.trace(e);
System.exit(1);
} catch (SOPGPException.BadData badData) { } catch (SOPGPException.BadData badData) {
Print.errln("Bad data in key file " + keyFile.getAbsolutePath() + ":"); String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput);
Print.trace(badData); throw new SOPGPException.BadData(errorMsg, badData);
System.exit(badData.getExitCode());
} }
} }
if (!armor) { if (!armor) {
sign.noArmor(); detachedSign.noArmor();
} }
try { try {
ReadyWithResult<SigningResult> ready = sign.data(System.in); ReadyWithResult<SigningResult> ready = detachedSign.data(System.in);
SigningResult result = ready.writeTo(System.out); SigningResult result = ready.writeTo(System.out);
MicAlg micAlg = result.getMicAlg(); MicAlg micAlg = result.getMicAlg();
if (micAlgOut != null) { if (micAlgOut != null) {
// Write micalg out // Write micalg out
micAlgOut.createNewFile(); OutputStream outputStream = getOutput(micAlgOut);
FileOutputStream micAlgOutStream = new FileOutputStream(micAlgOut); micAlg.writeTo(outputStream);
micAlg.writeTo(micAlgOutStream); outputStream.close();
micAlgOutStream.close();
} }
} catch (IOException e) { } catch (IOException e) {
Print.errln("IO Error."); throw new RuntimeException(e);
Print.trace(e);
System.exit(1);
} catch (SOPGPException.ExpectedText expectedText) {
Print.errln("Expected text input, but got binary data.");
Print.trace(expectedText);
System.exit(expectedText.getExitCode());
} }
} }
} }

View file

@ -4,129 +4,101 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import picocli.CommandLine; import picocli.CommandLine;
import sop.Verification; import sop.Verification;
import sop.cli.picocli.DateParser;
import sop.cli.picocli.Print; import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI; import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.operation.Verify; import sop.operation.DetachedVerify;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@CommandLine.Command(name = "verify", @CommandLine.Command(name = "verify",
description = "Verify a detached signature over the data from standard input", resourceBundle = "sop",
exitCodeOnInvalidInput = 37) exitCodeOnInvalidInput = 37)
public class VerifyCmd extends AbstractSopCmd { public class VerifyCmd extends AbstractSopCmd {
@CommandLine.Parameters(index = "0", @CommandLine.Parameters(index = "0",
description = "Detached signature", descriptionKey = "sop.verify.usage.parameter.signature",
paramLabel = "SIGNATURE") paramLabel = "SIGNATURE")
File signature; String signature;
@CommandLine.Parameters(index = "0..*", @CommandLine.Parameters(index = "0..*",
arity = "1..*", arity = "1..*",
description = "Public key certificates", descriptionKey = "sop.verify.usage.parameter.certs",
paramLabel = "CERT") paramLabel = "CERT")
List<File> certificates = new ArrayList<>(); List<String> certificates = new ArrayList<>();
@CommandLine.Option(names = {"--not-before"}, @CommandLine.Option(names = {"--not-before"},
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + descriptionKey = "sop.verify.usage.option.not_before",
"Reject signatures with a creation date not in range.\n" +
"Defaults to beginning of time (\"-\").",
paramLabel = "DATE") paramLabel = "DATE")
String notBefore = "-"; String notBefore = "-";
@CommandLine.Option(names = {"--not-after"}, @CommandLine.Option(names = {"--not-after"},
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + descriptionKey = "sop.verify.usage.option.not_after",
"Reject signatures with a creation date not in range.\n" +
"Defaults to current system time (\"now\").\n" +
"Accepts special value \"-\" for end of time.",
paramLabel = "DATE") paramLabel = "DATE")
String notAfter = "now"; String notAfter = "now";
@Override @Override
public void run() { public void run() {
Verify verify = throwIfUnsupportedSubcommand( DetachedVerify detachedVerify = throwIfUnsupportedSubcommand(
SopCLI.getSop().verify(), "verify"); SopCLI.getSop().detachedVerify(), "verify");
if (notAfter != null) { if (notAfter != null) {
try { try {
verify.notAfter(DateParser.parseNotAfter(notAfter)); detachedVerify.notAfter(parseNotAfter(notAfter));
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--not-after'."); String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after");
Print.trace(unsupportedOption); throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
System.exit(unsupportedOption.getExitCode());
} }
} }
if (notBefore != null) { if (notBefore != null) {
try { try {
verify.notBefore(DateParser.parseNotBefore(notBefore)); detachedVerify.notBefore(parseNotBefore(notBefore));
} catch (SOPGPException.UnsupportedOption unsupportedOption) { } catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--not-before'."); String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before");
Print.trace(unsupportedOption); throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
System.exit(unsupportedOption.getExitCode());
} }
} }
for (File certFile : certificates) { for (String certInput : certificates) {
try (FileInputStream certIn = new FileInputStream(certFile)) { try (InputStream certIn = getInput(certInput)) {
verify.cert(certIn); detachedVerify.cert(certIn);
} catch (FileNotFoundException fileNotFoundException) {
Print.errln("Certificate file " + certFile.getAbsolutePath() + " not found.");
Print.trace(fileNotFoundException);
System.exit(1);
} catch (IOException ioException) { } catch (IOException ioException) {
Print.errln("IO Error."); throw new RuntimeException(ioException);
Print.trace(ioException);
System.exit(1);
} catch (SOPGPException.BadData badData) { } catch (SOPGPException.BadData badData) {
Print.errln("Certificate file " + certFile.getAbsolutePath() + " appears to not contain a valid OpenPGP certificate."); String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput);
Print.trace(badData); throw new SOPGPException.BadData(errorMsg, badData);
System.exit(badData.getExitCode());
} }
} }
if (signature != null) { if (signature != null) {
try (FileInputStream sigIn = new FileInputStream(signature)) { try (InputStream sigIn = getInput(signature)) {
verify.signatures(sigIn); detachedVerify.signatures(sigIn);
} catch (FileNotFoundException e) {
Print.errln("Signature file " + signature.getAbsolutePath() + " does not exist.");
Print.trace(e);
System.exit(1);
} catch (IOException e) { } catch (IOException e) {
Print.errln("IO Error."); throw new RuntimeException(e);
Print.trace(e);
System.exit(1);
} catch (SOPGPException.BadData badData) { } catch (SOPGPException.BadData badData) {
Print.errln("File " + signature.getAbsolutePath() + " does not contain a valid OpenPGP signature."); String errorMsg = getMsg("sop.error.input.not_a_signature", signature);
Print.trace(badData); throw new SOPGPException.BadData(errorMsg, badData);
System.exit(badData.getExitCode());
} }
} }
List<Verification> verifications = null; List<Verification> verifications;
try { try {
verifications = verify.data(System.in); verifications = detachedVerify.data(System.in);
} catch (SOPGPException.NoSignature e) { } catch (SOPGPException.NoSignature e) {
Print.errln("No verifiable signature found."); String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found");
Print.trace(e); throw new SOPGPException.NoSignature(errorMsg, e);
System.exit(e.getExitCode());
} catch (IOException ioException) { } catch (IOException ioException) {
Print.errln("IO Error."); throw new RuntimeException(ioException);
Print.trace(ioException);
System.exit(1);
} catch (SOPGPException.BadData badData) { } catch (SOPGPException.BadData badData) {
Print.errln("Standard Input appears not to contain a valid OpenPGP message."); String errorMsg = getMsg("sop.error.input.stdin_not_a_message");
Print.trace(badData); throw new SOPGPException.BadData(errorMsg, badData);
System.exit(badData.getExitCode());
} }
for (Verification verification : verifications) { for (Verification verification : verifications) {
Print.outln(verification.toString()); Print.outln(verification.toString());
} }

View file

@ -9,7 +9,7 @@ import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI; import sop.cli.picocli.SopCLI;
import sop.operation.Version; import sop.operation.Version;
@CommandLine.Command(name = "version", description = "Display version information about the tool", @CommandLine.Command(name = "version", resourceBundle = "sop",
exitCodeOnInvalidInput = 37) exitCodeOnInvalidInput = 37)
public class VersionCmd extends AbstractSopCmd { public class VersionCmd extends AbstractSopCmd {
@ -17,10 +17,12 @@ public class VersionCmd extends AbstractSopCmd {
Exclusive exclusive; Exclusive exclusive;
static class Exclusive { static class Exclusive {
@CommandLine.Option(names = "--extended", description = "Print an extended version string.") @CommandLine.Option(names = "--extended",
descriptionKey = "sop.version.usage.option.extended")
boolean extended; boolean extended;
@CommandLine.Option(names = "--backend", description = "Print information about the cryptographic backend.") @CommandLine.Option(names = "--backend",
descriptionKey = "sop.version.usage.option.backend")
boolean backend; boolean backend;
} }

View file

@ -0,0 +1,157 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
sop.name=sop
usage.header=Stateless OpenPGP Protocol
usage.footerHeading=Powered by picocli%n
sop.locale=Locale for description texts
# Generic
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
# Exit Codes
usage.exitCodeListHeading=%nExit Codes:%n
usage.exitCodeList.0=\u00200:Successful program execution.
usage.exitCodeList.1=\u00201:Generic program error
usage.exitCodeList.2=\u00203:Verification requested but no verifiable signature found
usage.exitCodeList.3=13:Unsupported asymmetric algorithm
usage.exitCodeList.4=17:Certificate is not encryption capable
usage.exitCodeList.5=19:Usage error: Missing argument
usage.exitCodeList.6=23:Incomplete verification instructions
usage.exitCodeList.7=29:Unable to decrypt
usage.exitCodeList.8=31:Password is not human-readable
usage.exitCodeList.9=37:Unsupported Option
usage.exitCodeList.10=41:Invalid data or data of wrong type encountered
usage.exitCodeList.11=53:Non-text input received where text was expected
usage.exitCodeList.12=59:Output file already exists
usage.exitCodeList.13=61:Input file does not exist
usage.exitCodeList.14=67:Cannot unlock password protected secret key
usage.exitCodeList.15=69:Unsupported subcommand
usage.exitCodeList.16=71:Unsupported special prefix (e.g. \"@env/@fd\") of indirect parameter
usage.exitCodeList.17=73:Ambiguous input (a filename matching the designator already exists)
usage.exitCodeList.18=79:Key is not signing capable
# Subcommands
sop.armor.usage.header=Add ASCII Armor to standard input
sop.armor.usage.option.label=Label to be used in the header and tail of the armoring
sop.dearmor.usage.header=Remove ASCII Armor from standard input
sop.decrypt.usage.header=Decrypt a message from standard input
sop.decrypt.usage.option.session_key_out=Can be used to learn the session key on successful decryption
sop.decrypt.usage.option.with_session_key.0=Symmetric message key (session key).
sop.decrypt.usage.option.with_session_key.1=Enables decryption of the "CIPHERTEXT" using the session key directly against the "SEIPD" packet.
sop.decrypt.usage.option.with_session_key.2=Is an INDIRECT data type (eg. file, environment variable, file descriptor...)
sop.decrypt.usage.option.with_password.0=Symmetric passphrase to decrypt the message with.
sop.decrypt.usage.option.with_password.1=Enables decryption based on any "SKESK" packets in the "CIPHERTEXT".
sop.decrypt.usage.option.with_password_2=Is an INDIRECT data type (eg. file, environment variable, file descriptor...)
sop.decrypt.usage.option.verify_out=Emits signature verification status to the designated output
sop.decrypt.usage.option.certs=Certificates for signature verification
sop.decrypt.usage.option.not_before.0=ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)
sop.decrypt.usage.option.not_before.1=Reject signatures with a creation date not in range.
sop.decrypt.usage.option.not_before.2=Defaults to beginning of time ('-').
sop.decrypt.usage.option.not_after.0=ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)
sop.decrypt.usage.option.not_after.1=Reject signatures with a creation date not in range.
sop.decrypt.usage.option.not_after.2=Defaults to current system time ('now').
sop.decrypt.usage.option.not_after.3=Accepts special value '-' for end of time.
sop.decrypt.usage.option.with_key_password.0=Passphrase to unlock the secret key(s).
sop.decrypt.usage.option.with_key_password.1=Is an INDIRECT data type (eg. file, environment variable, file descriptor...).
sop.decrypt.usage.param.keys=Secret keys to attempt decryption with
sop.encrypt.usage.header=Encrypt a message from standard input
sop.encrypt.usage.option.armor=ASCII armor the output
sop.encrypt.usage.option.type=Type of the input data. Defaults to 'binary'
sop.encrypt.usage.option.with_password.0=Encrypt the message with a password.
sop.encrypt.usage.option.with_password.1=Is an INDIRECT data type (eg. file, environment variable, file descriptor...)
sop.encrypt.usage.option.sign_with=Sign the output with a private key
sop.encrypt.usage.option.with_key_password.0=Passphrase to unlock the secret key(s).
sop.encrypt.usage.option.with_key_password.1=Is an INDIRECT data type (eg. file, environment variable, file descriptor...).
sop.encrypt.usage.param.certs=Certificates the message gets encrypted to
sop.extract-cert.usage.header=Extract a public key certificate from a secret key from standard input
sop.extract-cert.usage.option.armor=ASCII armor the output
sop.generate-key.usage.header=Generate a secret key
sop.generate-key.usage.option.armor=ASCII armor the output
sop.generate-key.usage.option.user_id=User-ID, eg. "Alice <alice@example.com>"
sop.generate-key.usage.option.with_key_password.0=Password to protect the private key with
sop.generate-key.usage.option.with_key_password.1=Is an INDIRECT data type (eg. file, environment variable, file descriptor...).
sop.inline-detach.usage.header=Split signatures from a clearsigned message
sop.inline-detach.usage.option.armor=ASCII armor the output
sop.inline-detach.usage.option.signatures_out=Destination to which a detached signatures block will be written
sop.inline-sign.usage.header=Create an inline-signed message from data on standard input
sop.inline-sign.usage.option.armor=ASCII armor the output
sop.inline-sign.usage.option.as.0=Specify the signature format of the signed message
sop.inline-sign.usage.option.as.1='text' and 'binary' will produce inline-signed messages.
sop.inline-sign.usage.option.as.2='cleartextsigned' will make use of the cleartext signature framework.
sop.inline-sign.usage.option.as.3=Defaults to 'binary'.
sop.inline-sign.usage.option.as.4=If '--as=text' and the input data is not valid UTF-8, inline-sign fails with return code 53.
sop.inline-sign.usage.option.with_key_password.0=Passphrase to unlock the secret key(s).
sop.inline-sign.usage.option.with_key_password.1=Is an INDIRECT data type (eg. file, environment variable, file descriptor...).
sop.inline-sign.usage.option.micalg=Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156)
sop.inline-sign.usage.parameter.keys=Secret keys used for signing
sop.inline-verify.usage.header=Verify inline-signed data from standard input
sop.inline-verify.usage.option.not_before.0=ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)
sop.inline-verify.usage.option.not_before.1=Reject signatures with a creation date not in range.
sop.inline-verify.usage.option.not_before.2=Defaults to beginning of time ("-").
sop.inline-verify.usage.option.not_after.0=ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)
sop.inline-verify.usage.option.not_after.1=Reject signatures with a creation date not in range.
sop.inline-verify.usage.option.not_after.2=Defaults to current system time ("now").
sop.inline-verify.usage.option.not_after.3=Accepts special value "-" for end of time.
sop.inline-verify.usage.option.verifications_out=File to write details over successful verifications to
sop.inline-verify.usage.parameter.certs=Public key certificates for signature verification
sop.sign.usage.header=Create a detached signature on the data from standard input
sop.sign.usage.option.armor=ASCII armor the output
sop.sign.usage.option.as.0=Specify the output format of the signed message
sop.sign.usage.option.as.1=Defaults to 'binary'.
sop.sign.usage-option.as.2=If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53.
sop.sign.usage.option.with_key_password.0=Passphrase to unlock the secret key(s).
sop.sign.usage.option.with_key_password.1=Is an INDIRECT data type (eg. file, environment variable, file descriptor...).
sop.sign.usage.option.micalg_out=Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156)
sop.sign.usage.parameter.keys=Secret keys used for signing
sop.verify.usage.header=Verify a detached signature over the data from standard input
sop.verify.usage.option.not_before.0=ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)
sop.verify.usage.option.not_before.1=Reject signatures with a creation date not in range.
sop.verify.usage.option.not_before.2=Defaults to beginning of time ("-").
sop.verify.usage.option.not_after.1=ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)
sop.verify.usage.option.not_after.2=Reject signatures with a creation date not in range.
sop.verify.usage.option.not_after.3=Defaults to current system time ("now").\
sop.verify.usage.option.not_after.4 = Accepts special value "-" for end of time.
sop.verify.usage.parameter.signature=Detached signature
sop.verify.usage.parameter.certs=Public key certificates for signature verification
sop.version.usage.header=Display version information about the tool
sop.version.usage.option.extended=Print an extended version string
sop.version.usage.option.backend=Print information about the cryptographic backend
sop.help.usage.header=Display usage information for the specified subcommand
## Malformed Input
sop.error.input.malformed_session_key=Session keys are expected in the format 'ALGONUM:HEXKEY'.
sop.error.input.not_a_private_key=Input '{0}' does not contain an OpenPGP private key.
sop.error.input.not_a_certificate=Input '{0}' does not contain an OpenPGP certificate.
sop.error.input.not_a_signature=Input '{0}' does not contain an OpenPGP signature.
sop.error.input.malformed_not_after=Invalid date string supplied as value of '--not-after'.
sop.error.input.malformed_not_before=Invalid date string supplied as value of '--not-before'.
sop.error.input.stdin_not_a_message=Standard Input appears not to contain a valid OpenPGP message.
sop.error.input.stdin_not_a_private_key=Standard Input appears not to contain a valid OpenPGP secret key.
sop.error.input.stdin_not_openpgp_data=Standard Input appears not to contain valid OpenPGP data
## Indirect Data Types
sop.error.indirect_data_type.ambiguous_filename=File name '{0}' is ambiguous. File with the same name exists on the filesystem.
sop.error.indirect_data_type.environment_variable_not_set=Environment variable '{0}' not set.
sop.error.indirect_data_type.environment_variable_empty=Environment variable '{0}' is empty.
sop.error.indirect_data_type.input_file_does_not_exist=Input file '{0}' does not exist.
sop.error.indirect_data_type.input_not_a_file=Input file '{0}' is not a file.
sop.error.indirect_data_type.output_file_already_exists=Output file '{0}' already exists.
sop.error.indirect_data_type.output_file_cannot_be_created=Output file '{0}' cannot be created.
sop.error.indirect_data_type.illegal_use_of_env_designator=Special designator '@ENV:' cannot be used for output.
sop.error.indirect_data_type.designator_env_not_supported=Special designator '@ENV' is not supported.
sop.error.indirect_data_type.designator_fd_not_supported=Special designator '@FD' is not supported.
## Runtime Errors
sop.error.runtime.no_backend_set=No SOP backend set.
sop.error.runtime.cannot_unlock_key=Cannot unlock password-protected secret key from input '{0}'.
sop.error.runtime.key_uses_unsupported_asymmetric_algorithm=Secret key from input '{0}' uses an unsupported asymmetric algorithm.
sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm=Certificate from input '{0}' uses an unsupported asymmetric algorithm.
sop.error.runtime.key_cannot_sign=Secret key from input '{0}' cannot sign.
sop.error.runtime.cert_cannot_encrypt=Certificate from input '{0}' cannot encrypt.
sop.error.runtime.no_session_key_extracted=Session key not extracted. Feature potentially not supported.
sop.error.runtime.no_verifiable_signature_found=No verifiable signature found.
## Usage errors
sop.error.usage.password_or_cert_required=At least one password file or cert file required for encryption.
sop.error.usage.argument_required=Argument '{0}' is required.
sop.error.usage.parameter_required=Parameter '{0}' is required.
sop.error.usage.option_requires_other_option=Option '{0}' is requested, but no option {1} was provided.
# Feature Support
sop.error.feature_support.subcommand_not_supported=Subcommand '{0}' is not supported.
sop.error.feature_support.option_not_supported=Option '{0}' not supported.

View file

@ -0,0 +1,156 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
sop.name=sop
usage.header=Stateless OpenPGP Protocol
usage.footerHeading=Powered by Picocli%n
sop.locale=Gebietsschema für Beschreibungstexte
# Generic
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
# Exit Codes
usage.exitCodeListHeading=%nExit Codes:%n
usage.exitCodeList.0=\u00200:Erfolgreiche Programmausführung
usage.exitCodeList.1=\u00201:Generischer Programmfehler
usage.exitCodeList.2=\u00203:Signaturverifikation gefordert, aber keine gültige Signatur gefunden
usage.exitCodeList.3=13:Nicht unterstützter asymmetrischer Algorithmus
usage.exitCodeList.4=17:Zertifikat ist nicht fähig zu verschlüsseln
usage.exitCodeList.5=19:Nutzungsfehler: Fehlendes Argument
usage.exitCodeList.6=23:Unvollständige Verifikationsanweisungen
usage.exitCodeList.7=29:Entschlüsselung nicht möglich
usage.exitCodeList.8=31:Passwort ist nicht für Menschen lesbar
usage.exitCodeList.9=37:Nicht unterstützte Option
usage.exitCodeList.10=41:Ungültige Daten oder Daten des falschen Typs gefunden
usage.exitCodeList.11=53:Nicht-Text-Eingabe erhalten, wo Text erwartet wurde
usage.exitCodeList.12=59:Ausgabedatei existiert bereits
usage.exitCodeList.13=61:Eingabedatei existiert nicht
usage.exitCodeList.14=67:Passwort-gesicherter privater Schlüssel kann nicht entsperrt werden
usage.exitCodeList.15=69:Nicht unterstützter Unterbefehl
usage.exitCodeList.16=71:Nicht unterstützter Spezialprefix (zB. "@env/@fd") von indirektem Parameter
usage.exitCodeList.17=73:Mehrdeutige Eingabe (ein Dateiname, der dem Bezeichner entspricht, existiert bereits)
usage.exitCodeList.18=79:Schlüssel ist nicht fähig zu signieren
# Subcommands
sop.armor.usage.header=Schütze Standard-Eingabe mit ASCII Armor
sop.armor.usage.option.label=Label für Kopf- und Fußzeile der ASCII Armor
sop.dearmor.usage.header=Entferne ASCII Armor von Standard-Eingabe
sop.decrypt.usage.header=Entschlüssle eine Nachricht von Standard-Eingabe
sop.decrypt.usage.option.session_key_out=Extrahiere den Nachrichtenschlüssel nach erfolgreicher Entschlüsselung
sop.decrypt.usage.option.with_session_key.0=Symmetrischer Nachrichtenschlüssel (Sitzungsschlüssel).
sop.decrypt.usage.option.with_session_key.1=Ermöglicht direkte Entschlüsselung des im "CIPHERTEXT" enhaltenen "SEIPD" Paketes mithilfe des Nachrichtenschlüssels.
sop.decrypt.usage.option.with_session_key.2=Ist INDIREKTER Datentyp (zb. Datei, Umgebungsvariable, Dateideskriptor...).
sop.decrypt.usage.option.with_password.0=Symmetrisches Passwort zur Entschlüsselung der Nachricht.
sop.decrypt.usage.option.with_password.1=Ermöglicht Entschlüsselung basierend auf im "CIPHERTEXT" enthaltenen "SKESK" Paketen.
sop.decrypt.usage.option.with_password.2=Ist INDIREKTER Datentyp (zb. Datei, Umgebungsvariable, Dateideskriptor...).
sop.decrypt.usage.option.verify_out=Schreibe Status der Signaturprüfung in angegebene Ausgabe
sop.decrypt.usage.option.certs=Zertifikate zur Signaturprüfung
sop.decrypt.usage.option.not_before.0=Nach ISO-8601 formatierter UTC Zeitstempel (zB. '2020-11-23T16:35Z)
sop.decrypt.usage.option.not_before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab.
sop.decrypt.usage.option.not_before.2=Standardmäßig: Anbeginn der Zeit ('-').
sop.decrypt.usage.option.not_after.0=Nach ISO-8601 formatierter UTC Zeitstempel (zB. '2020-11-23T16:35Z)
sop.decrypt.usage.option.not_after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab.
sop.decrypt.usage.option.not_after.2=Standardmäßig: Aktueller Zeitunkt ('now').
sop.decrypt.usage.option.not_after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten.
sop.decrypt.usage.option.with_key_password.0=Passwort zum Entsperren der privaten Schlüssel
sop.decrypt.usage.option.with_key_password.1=Ist INDIREKTER Datentyp (zb. Datei, Umgebungsvariable, Dateideskriptor...).
sop.decrypt.usage.param.keys=Private Schlüssel zum Entschlüsseln der Nachricht
sop.encrypt.usage.header=Verschlüssle eine Nachricht von Standard-Eingabe
sop.encrypt.usage.option.armor=Schütze Ausgabe mit ASCII Armor
sop.encrypt.usage.option.type=Format der Nachricht. Standardmäßig 'binary'
sop.encrypt.usage.option.with_password.0=Verschlüssle die Nachricht mit einem Passwort
sop.encrypt.usage.option.with_password.1=Ist ein INDIREKTER Datentyp (zB. Datei, Umgebungsvariable, Dateideskriptor...).
sop.encrypt.usage.option.sign_with=Signiere die Nachricht mit einem privaten Schlüssel
sop.encrypt.usage.option.with_key_password.0=Passwort zum Entsperren der privaten Schlüssel
sop.encrypt.usage.option.with_key_password.1=Ist INDIREKTER Datentyp (zb. Datei, Umgebungsvariable, Dateideskriptor...).
sop.encrypt.usage.param.certs=Zertifikate für die die Nachricht verschlüsselt werden soll
sop.extract-cert.usage.header=Extrahiere Zertifikat (öffentlichen Schlüssel) aus privatem Schlüssel von Standard-Eingabe
sop.extract-cert.usage.option.armor=Schütze Ausgabe mit ASCII Armor
sop.generate-key.usage.header=Generiere einen privaten Schlüssel
sop.generate-key.usage.option.armor=Schütze Ausgabe mit ASCII Armor
sop.generate-key.usage.option.user_id=Nutzer-ID, zB. "Alice <alice@example.com>"
sop.generate-key.usage.option.with_key_password.0=Passwort zum Schutz des privaten Schlüssels
sop.generate-key.usage.option.with_key_password.1=Ist ein INDIREKTER Datentyp (zB. Datei, Umgebungsvariable, Dateideskriptor...).
sop.inline-detach.usage.header=Trenne Signaturen von Klartext-signierter Nachricht
sop.inline-detach.usage.option.armor=Schütze Ausgabe mit ASCII Armor
sop.inline-detach.usage.option.signatures_out=Schreibe abgetrennte Signaturen in Ausgabe
sop.inline-sign.usage.header=Signiere eine Nachricht von Standard-Eingabe mit eingebetteten Signaturen
sop.inline-sign.usage.option.armor=Schütze Ausgabe mit ASCII Armor
sop.inline-sign.usage.option.as.0=Bestimme Signaturformat der Nachricht.
sop.inline-sign.usage.option.as.1='text' und 'binary' resultieren in eingebettete Signaturen.
sop.inline-sign.usage.option.as.2='cleartextsigned' wird die Nachricht Klartext-signieren.
sop.inline-sign.usage.option.as.3=Standardmäßig: 'binary'.
sop.inline-sign.usage.option.as.4=Ist die Standard-Eingabe nicht UTF-8 kodiert und '--as=text' gesetzt, so wird inline-sign Fehlercode 53 zurückgeben.
sop.inline-sign.usage.option.with_key_password.0=Passwort zum Entsperren des privaten Schlüssels
sop.inline-sign.usage.option.with_key_password.1=Ist ein INDIREKTER Datentyp (zB. Datei, Umgebungsvariable, Dateideskriptor...).
sop.inline-sign.usage.option.micalg=Gibt den verwendeten Digest-Algorithmus an die angegebene Ausgabe in einer Form aus, die zum Auffüllen des micalg-Parameters für den PGP/MIME Content-Type (RFC3156) verwendet werden kann.
sop.inline-sign.usage.parameter.keys=Private Signaturschlüssel
sop.inline-verify.usage.header=Prüfe eingebettete Signaturen einer Nachricht von Standard-Eingabe
sop.inline-verify.usage.option.not_before.0=Nach ISO-8601 formatierter UTC Zeitstempel (zB. '2020-11-23T16:35Z)
sop.inline-verify.usage.option.not_before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab.
sop.inline-verify.usage.option.not_before.2=Standardmäßig: Anbeginn der Zeit ('-').
sop.inline-verify.usage.option.not_after.0=Nach ISO-8601 formatierter UTC Zeitstempel (zB. '2020-11-23T16:35Z)
sop.inline-verify.usage.option.not_after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab.
sop.inline-verify.usage.option.not_after.2=Standardmäßig: Aktueller Zeitunkt ('now').
sop.inline-verify.usage.option.not_after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten.
sop.inline-verify.usage.option.verifications_out=Schreibe Status der Signaturprüfung in angegebene Ausgabe
sop.inline-verify.usage.parameter.certs=Zertifikate (öffentlich Schlüssel) zur Signaturprüfung
sop.sign.usage.header=Erstelle abgetrennte Signatur über Nachricht von Standard-Eingabe
sop.sign.usage.option.armor=Schütze Ausgabe mit ASCII Armor
sop.sign.usage.option.as.0=Bestimme Signaturformat der Nachricht.
sop.sign.usage.option.as.1=Standardmäßig: 'binary'.
sop.sign.usage-option.as.2=Ist die Standard-Eingabe nicht UTF-8 kodiert und '--as=text' gesetzt, so wird inline-sign Fehlercode 53 zurückgeben.
sop.sign.usage.option.with_key_password.0=Passwort zum Entsperren des privaten Schlüssels
sop.sign.usage.option.with_key_password.1=Ist ein INDIREKTER Datentyp (zB. Datei, Umgebungsvariable, Dateideskriptor...).
sop.sign.usage.option.micalg_out=Gibt den verwendeten Digest-Algorithmus an die angegebene Ausgabe in einer Form aus, die zum Auffüllen des micalg-Parameters für den PGP/MIME Content-Type (RFC3156) verwendet werden kann.
sop.sign.usage.parameter.keys=Private Signaturschlüssel
sop.verify.usage.header=Prüfe eine abgetrennte Signatur über eine Nachricht von Standard-Eingabe
sop.verify.usage.option.not_before.0=Nach ISO-8601 formatierter UTC Zeitstempel (zB. '2020-11-23T16:35Z)
sop.verify.usage.option.not_before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab.
sop.verify.usage.option.not_before.2=Standardmäßig: Anbeginn der Zeit ('-').
sop.verify.usage.option.not_after.0=Nach ISO-8601 formatierter UTC Zeitstempel (zB. '2020-11-23T16:35Z)
sop.verify.usage.option.not_after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab.
sop.verify.usage.option.not_after.2=Standardmäßig: Aktueller Zeitunkt ('now').
sop.verify.usage.option.not_after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten.
sop.verify.usage.parameter.signature=Abgetrennte Signatur
sop.verify.usage.parameter.certs=Zertifikate (öffentliche Schlüssel) zur Signaturprüfung
sop.version.usage.header=Zeige Versionsinformationen über das Programm
sop.version.usage.option.extended=Gebe erweiterte Versionsinformationen aus
sop.version.usage.option.backend=Gebe Informationen über das kryptografische Backend aus
sop.help.usage.header=Zeige Nutzungshilfen für den angegebenen Unterbefehl an
## Malformed Input
sop.error.input.malformed_session_key=Nachrichtenschlüssel werden im folgenden Format erwartet: 'ALGONUM:HEXKEY'
sop.error.input.not_a_private_key=Eingabe '{0}' enthält keinen privaten OpenPGP Schlüssel.
sop.error.input.not_a_certificate=Eingabe '{0}' enthält kein OpenPGP Zertifikat.
sop.error.input.not_a_signature=Eingabe '{0}' enthält keine OpenPGP Signatur.
sop.error.input.malformed_not_after=Ungültige Datumszeichenfolge als Wert von '--not-after'.
sop.error.input.malformed_not_before=Ungültige Datumszeichenfolge als Wert von '--not-before'.
sop.error.input.stdin_not_a_message=Standard-Eingabe enthält scheinbar keine OpenPGP Nachricht.
sop.error.input.stdin_not_a_private_key=Standard-Eingabe enthält scheinbar keinen privaten OpenPGP Schlüssel.
sop.error.input.stdin_not_openpgp_data=Standard-Eingabe enthält scheinbar keine gültigen OpenPGP Daten.
## Indirect Data Types
sop.error.indirect_data_type.ambiguous_filename=Dateiname '{0}' ist mehrdeutig. Datei mit dem selben Namen existiert im Dateisystem.
sop.error.indirect_data_type.environment_variable_not_set=Umgebungsvariable '{0}' nicht gesetzt.
sop.error.indirect_data_type.environment_variable_empty=Umgebungsvariable '{0}' ist leer.
sop.error.indirect_data_type.input_file_does_not_exist=Quelldatei '{0}' existiert nicht.
sop.error.indirect_data_type.input_not_a_file=Quelldatei '{0}' ist keine Datei.
sop.error.indirect_data_type.output_file_already_exists=Zieldatei '{0}' existiert bereits.
sop.error.indirect_data_type.output_file_cannot_be_created=Zieldatei '{0}' kann nicht erstellt werden.
sop.error.indirect_data_type.illegal_use_of_env_designator=Besonderer Bezeichner-Präfix '@ENV:' darf nicht für Ausgaben verwendet werden.
sop.error.indirect_data_type.designator_env_not_supported=Besonderer Bezeichner-Präfix '@ENV' wird nicht unterstützt.
sop.error.indirect_data_type.designator_fd_not_supported=Besonderer Bezeichner-Präfix '@FD' wird nicht unterstützt.
## Runtime Errors
sop.error.runtime.no_backend_set=Kein SOP Backend gesetzt.
sop.error.runtime.cannot_unlock_key=Gesperrter Schlüssel aus Eingabe '{0}' kann nicht entsperrt werden.
sop.error.runtime.key_uses_unsupported_asymmetric_algorithm=Privater Schlüssel aus Eingabe '{0}' nutzt nicht unterstütztem asymmetrischen Algorithmus.
sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm=Zertifikat aus Eingabe '{0}' nutzt nicht unterstütztem asymmetrischen Algorithmus.
sop.error.runtime.key_cannot_sign=Privater Schlüssel aus Eingabe '{0}' kann nicht signieren.
sop.error.runtime.cert_cannot_encrypt=Zertifikat aus Eingabe '{0}' kann nicht verschlüsseln.
sop.error.runtime.no_session_key_extracted=Nachrichtenschlüssel nicht extrahiert. Funktion wird möglicherweise nicht unterstützt.
sop.error.runtime.no_verifiable_signature_found=Keine gültigen Signaturen gefunden.
## Usage errors
sop.error.usage.password_or_cert_required=Es wird mindestens ein Passwort und/oder Zertifikat zur Verschlüsselung benötigt.
sop.error.usage.argument_required=Argument '{0}' ist erforderlich.
sop.error.usage.parameter_required=Parameter '{0}' ist erforderlich.
sop.error.usage.option_requires_other_option=Option '{0}' wurde angegeben, jedoch kein Wert für {1}.
# Feature Support
sop.error.feature_support.subcommand_not_supported=Unterbefehl '{0}' wird nicht unterstützt.
sop.error.feature_support.option_not_supported=Option '{0}' wird nicht unterstützt.

View file

@ -9,41 +9,44 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Date; import java.util.Date;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sop.cli.picocli.commands.AbstractSopCmd;
import sop.cli.picocli.commands.ArmorCmd;
import sop.util.UTCUtil; import sop.util.UTCUtil;
public class DateParserTest { public class DateParsingTest {
private AbstractSopCmd cmd = new ArmorCmd(); // we use ArmorCmd as a concrete implementation.
@Test @Test
public void parseNotAfterDashReturnsEndOfTime() { public void parseNotAfterDashReturnsEndOfTime() {
assertEquals(DateParser.END_OF_TIME, DateParser.parseNotAfter("-")); assertEquals(AbstractSopCmd.END_OF_TIME, cmd.parseNotAfter("-"));
} }
@Test @Test
public void parseNotBeforeDashReturnsBeginningOfTime() { public void parseNotBeforeDashReturnsBeginningOfTime() {
assertEquals(DateParser.BEGINNING_OF_TIME, DateParser.parseNotBefore("-")); assertEquals(AbstractSopCmd.BEGINNING_OF_TIME, cmd.parseNotBefore("-"));
} }
@Test @Test
public void parseNotAfterNowReturnsNow() { public void parseNotAfterNowReturnsNow() {
assertEquals(new Date().getTime(), DateParser.parseNotAfter("now").getTime(), 1000); assertEquals(new Date().getTime(), cmd.parseNotAfter("now").getTime(), 1000);
} }
@Test @Test
public void parseNotBeforeNowReturnsNow() { public void parseNotBeforeNowReturnsNow() {
assertEquals(new Date().getTime(), DateParser.parseNotBefore("now").getTime(), 1000); assertEquals(new Date().getTime(), cmd.parseNotBefore("now").getTime(), 1000);
} }
@Test @Test
public void parseNotAfterTimestamp() { public void parseNotAfterTimestamp() {
String timestamp = "2019-10-24T23:48:29Z"; String timestamp = "2019-10-24T23:48:29Z";
Date date = DateParser.parseNotAfter(timestamp); Date date = cmd.parseNotAfter(timestamp);
assertEquals(timestamp, UTCUtil.formatUTCDate(date)); assertEquals(timestamp, UTCUtil.formatUTCDate(date));
} }
@Test @Test
public void parseNotBeforeTimestamp() { public void parseNotBeforeTimestamp() {
String timestamp = "2019-10-29T18:36:45Z"; String timestamp = "2019-10-29T18:36:45Z";
Date date = DateParser.parseNotBefore(timestamp); Date date = cmd.parseNotBefore(timestamp);
assertEquals(timestamp, UTCUtil.formatUTCDate(date)); assertEquals(timestamp, UTCUtil.formatUTCDate(date));
} }
} }

View file

@ -1,123 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import sop.exception.SOPGPException;
public class FileUtilTest {
@BeforeAll
public static void setup() {
FileUtil.setEnvironmentVariableResolver(new FileUtil.EnvironmentVariableResolver() {
@Override
public String resolveEnvironmentVariable(String name) {
if (name.equals("test123")) {
return "test321";
}
return null;
}
});
}
@Test
public void getFile_ThrowsForNull() {
assertThrows(NullPointerException.class, () -> FileUtil.getFile(null));
}
@Test
public void getFile_prfxEnvAlreadyExists() throws IOException {
File tempFile = new File("@ENV:test");
tempFile.createNewFile();
tempFile.deleteOnExit();
assertThrows(SOPGPException.AmbiguousInput.class, () -> FileUtil.getFile("@ENV:test"));
}
@Test
public void getFile_EnvironmentVariable() {
File file = FileUtil.getFile("@ENV:test123");
assertEquals("test321", file.getName());
}
@Test
public void getFile_nonExistentEnvVariable() {
assertThrows(IllegalArgumentException.class, () -> FileUtil.getFile("@ENV:INVALID"));
}
@Test
public void getFile_prfxFdAlreadyExists() throws IOException {
File tempFile = new File("@FD:1");
tempFile.createNewFile();
tempFile.deleteOnExit();
assertThrows(SOPGPException.AmbiguousInput.class, () -> FileUtil.getFile("@FD:1"));
}
@Test
public void getFile_prfxFdNotSupported() {
assertThrows(IllegalArgumentException.class, () -> FileUtil.getFile("@FD:2"));
}
@Test
public void createNewFileOrThrow_throwsForNull() {
assertThrows(NullPointerException.class, () -> FileUtil.createNewFileOrThrow(null));
}
@Test
public void createNewFileOrThrow_success() throws IOException {
File dir = Files.createTempDirectory("test").toFile();
dir.deleteOnExit();
File file = new File(dir, "file");
assertFalse(file.exists());
FileUtil.createNewFileOrThrow(file);
assertTrue(file.exists());
}
@Test
public void createNewFileOrThrow_alreadyExists() throws IOException {
File dir = Files.createTempDirectory("test").toFile();
dir.deleteOnExit();
File file = new File(dir, "file");
FileUtil.createNewFileOrThrow(file);
assertTrue(file.exists());
assertThrows(SOPGPException.OutputExists.class, () -> FileUtil.createNewFileOrThrow(file));
}
@Test
public void getFileInputStream_success() throws IOException {
File dir = Files.createTempDirectory("test").toFile();
dir.deleteOnExit();
File file = new File(dir, "file");
FileUtil.createNewFileOrThrow(file);
FileInputStream inputStream = FileUtil.getFileInputStream(file.getAbsolutePath());
assertNotNull(inputStream);
}
@Test
public void getFileInputStream_fileNotFound() throws IOException {
File dir = Files.createTempDirectory("test").toFile();
dir.deleteOnExit();
File file = new File(dir, "file");
assertThrows(SOPGPException.MissingInput.class,
() -> FileUtil.getFileInputStream(file.getAbsolutePath()));
}
}

View file

@ -23,8 +23,8 @@ import sop.operation.ExtractCert;
import sop.operation.GenerateKey; import sop.operation.GenerateKey;
import sop.operation.InlineSign; import sop.operation.InlineSign;
import sop.operation.InlineVerify; import sop.operation.InlineVerify;
import sop.operation.Sign; import sop.operation.DetachedSign;
import sop.operation.Verify; import sop.operation.DetachedVerify;
import sop.operation.Version; import sop.operation.Version;
public class SOPTest { public class SOPTest {
@ -65,12 +65,12 @@ public class SOPTest {
} }
@Override @Override
public Sign sign() { public DetachedSign detachedSign() {
return null; return null;
} }
@Override @Override
public Verify verify() { public DetachedVerify detachedVerify() {
return null; return null;
} }

View file

@ -36,7 +36,6 @@ import sop.ReadyWithResult;
import sop.SOP; import sop.SOP;
import sop.SessionKey; import sop.SessionKey;
import sop.Verification; import sop.Verification;
import sop.cli.picocli.DateParser;
import sop.cli.picocli.SopCLI; import sop.cli.picocli.SopCLI;
import sop.cli.picocli.TestFileUtil; import sop.cli.picocli.TestFileUtil;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
@ -116,7 +115,7 @@ public class DecryptCmdTest {
public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption { public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption {
Date now = new Date(); Date now = new Date();
SopCLI.main(new String[] {"decrypt"}); SopCLI.main(new String[] {"decrypt"});
verify(decrypt, times(1)).verifyNotBefore(DateParser.BEGINNING_OF_TIME); verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME);
verify(decrypt, times(1)).verifyNotAfter( verify(decrypt, times(1)).verifyNotAfter(
ArgumentMatchers.argThat(argument -> { ArgumentMatchers.argThat(argument -> {
// allow 1-second difference // allow 1-second difference
@ -127,8 +126,8 @@ public class DecryptCmdTest {
@Test @Test
public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption { public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"decrypt", "--not-before", "-", "--not-after", "-"}); SopCLI.main(new String[] {"decrypt", "--not-before", "-", "--not-after", "-"});
verify(decrypt, times(1)).verifyNotBefore(DateParser.BEGINNING_OF_TIME); verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME);
verify(decrypt, times(1)).verifyNotAfter(DateParser.END_OF_TIME); verify(decrypt, times(1)).verifyNotAfter(AbstractSopCmd.END_OF_TIME);
} }
@Test @Test

View file

@ -127,7 +127,7 @@ public class EncryptCmdTest {
@Test @Test
@ExpectSystemExitWithStatus(79) @ExpectSystemExitWithStatus(79)
public void signWith_certCannotSignCausesExit1() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { public void signWith_certCannotSignCausesExit79() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData {
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign()); when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign());
File keyFile = File.createTempFile("sign-with", ".asc"); File keyFile = File.createTempFile("sign-with", ".asc");
File passwordFile = TestFileUtil.writeTempStringFile("dragon"); File passwordFile = TestFileUtil.writeTempStringFile("dragon");

View file

@ -24,17 +24,17 @@ import sop.SOP;
import sop.SigningResult; import sop.SigningResult;
import sop.cli.picocli.SopCLI; import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.operation.Sign; import sop.operation.DetachedSign;
public class SignCmdTest { public class SignCmdTest {
Sign sign; DetachedSign detachedSign;
File keyFile; File keyFile;
@BeforeEach @BeforeEach
public void mockComponents() throws IOException, SOPGPException.ExpectedText { public void mockComponents() throws IOException, SOPGPException.ExpectedText {
sign = mock(Sign.class); detachedSign = mock(DetachedSign.class);
when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult<SigningResult>() { when(detachedSign.data((InputStream) any())).thenReturn(new ReadyWithResult<SigningResult>() {
@Override @Override
public SigningResult writeTo(OutputStream outputStream) { public SigningResult writeTo(OutputStream outputStream) {
return SigningResult.builder().build(); return SigningResult.builder().build();
@ -42,7 +42,7 @@ public class SignCmdTest {
}); });
SOP sop = mock(SOP.class); SOP sop = mock(SOP.class);
when(sop.sign()).thenReturn(sign); when(sop.detachedSign()).thenReturn(detachedSign);
SopCLI.setSopInstance(sop); SopCLI.setSopInstance(sop);
@ -65,27 +65,27 @@ public class SignCmdTest {
@Test @Test
@ExpectSystemExitWithStatus(37) @ExpectSystemExitWithStatus(37)
public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
when(sign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting signing mode not supported.")); when(detachedSign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting signing mode not supported."));
SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()});
} }
@Test @Test
@ExpectSystemExitWithStatus(1) @ExpectSystemExitWithStatus(61)
public void key_nonExistentKeyFileCausesExit1() { public void key_nonExistentKeyFileCausesExit61() {
SopCLI.main(new String[] {"sign", "invalid.asc"}); SopCLI.main(new String[] {"sign", "invalid.asc"});
} }
@Test @Test
@ExpectSystemExitWithStatus(1) @ExpectSystemExitWithStatus(67)
public void key_keyIsProtectedCausesExit1() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { public void key_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData {
when(sign.key((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
} }
@Test @Test
@ExpectSystemExitWithStatus(41) @ExpectSystemExitWithStatus(41)
public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData {
when(sign.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
} }
@ -98,19 +98,19 @@ public class SignCmdTest {
@Test @Test
public void noArmor_notCalledByDefault() { public void noArmor_notCalledByDefault() {
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
verify(sign, never()).noArmor(); verify(detachedSign, never()).noArmor();
} }
@Test @Test
public void noArmor_passedDown() { public void noArmor_passedDown() {
SopCLI.main(new String[] {"sign", "--no-armor", keyFile.getAbsolutePath()}); SopCLI.main(new String[] {"sign", "--no-armor", keyFile.getAbsolutePath()});
verify(sign, times(1)).noArmor(); verify(detachedSign, times(1)).noArmor();
} }
@Test @Test
@ExpectSystemExitWithStatus(1) @ExpectSystemExitWithStatus(1)
public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText { public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText {
when(sign.data((InputStream) any())).thenReturn(new ReadyWithResult<SigningResult>() { when(detachedSign.data((InputStream) any())).thenReturn(new ReadyWithResult<SigningResult>() {
@Override @Override
public SigningResult writeTo(OutputStream outputStream) throws IOException { public SigningResult writeTo(OutputStream outputStream) throws IOException {
throw new IOException(); throw new IOException();
@ -122,7 +122,7 @@ public class SignCmdTest {
@Test @Test
@ExpectSystemExitWithStatus(53) @ExpectSystemExitWithStatus(53)
public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText { public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText {
when(sign.data((InputStream) any())).thenThrow(new SOPGPException.ExpectedText()); when(detachedSign.data((InputStream) any())).thenThrow(new SOPGPException.ExpectedText());
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
} }
} }

View file

@ -27,15 +27,14 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers; import org.mockito.ArgumentMatchers;
import sop.SOP; import sop.SOP;
import sop.Verification; import sop.Verification;
import sop.cli.picocli.DateParser;
import sop.cli.picocli.SopCLI; import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.operation.Verify; import sop.operation.DetachedVerify;
import sop.util.UTCUtil; import sop.util.UTCUtil;
public class VerifyCmdTest { public class VerifyCmdTest {
Verify verify; DetachedVerify detachedVerify;
File signature; File signature;
File cert; File cert;
@ -45,12 +44,12 @@ public class VerifyCmdTest {
public void prepare() throws SOPGPException.UnsupportedOption, SOPGPException.BadData, SOPGPException.NoSignature, IOException { public void prepare() throws SOPGPException.UnsupportedOption, SOPGPException.BadData, SOPGPException.NoSignature, IOException {
originalSout = System.out; originalSout = System.out;
verify = mock(Verify.class); detachedVerify = mock(DetachedVerify.class);
when(verify.notBefore(any())).thenReturn(verify); when(detachedVerify.notBefore(any())).thenReturn(detachedVerify);
when(verify.notAfter(any())).thenReturn(verify); when(detachedVerify.notAfter(any())).thenReturn(detachedVerify);
when(verify.cert((InputStream) any())).thenReturn(verify); when(detachedVerify.cert((InputStream) any())).thenReturn(detachedVerify);
when(verify.signatures((InputStream) any())).thenReturn(verify); when(detachedVerify.signatures((InputStream) any())).thenReturn(detachedVerify);
when(verify.data((InputStream) any())).thenReturn( when(detachedVerify.data((InputStream) any())).thenReturn(
Collections.singletonList( Collections.singletonList(
new Verification( new Verification(
UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"), UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"),
@ -60,7 +59,7 @@ public class VerifyCmdTest {
); );
SOP sop = mock(SOP.class); SOP sop = mock(SOP.class);
when(sop.verify()).thenReturn(verify); when(sop.detachedVerify()).thenReturn(detachedVerify);
SopCLI.setSopInstance(sop); SopCLI.setSopInstance(sop);
@ -77,26 +76,26 @@ public class VerifyCmdTest {
public void notAfter_passedDown() throws SOPGPException.UnsupportedOption { public void notAfter_passedDown() throws SOPGPException.UnsupportedOption {
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"); Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z");
SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(verify, times(1)).notAfter(date); verify(detachedVerify, times(1)).notAfter(date);
} }
@Test @Test
public void notAfter_now() throws SOPGPException.UnsupportedOption { public void notAfter_now() throws SOPGPException.UnsupportedOption {
Date now = new Date(); Date now = new Date();
SopCLI.main(new String[] {"verify", "--not-after", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); SopCLI.main(new String[] {"verify", "--not-after", "now", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(verify, times(1)).notAfter(dateMatcher(now)); verify(detachedVerify, times(1)).notAfter(dateMatcher(now));
} }
@Test @Test
public void notAfter_dashCountsAsEndOfTime() throws SOPGPException.UnsupportedOption { public void notAfter_dashCountsAsEndOfTime() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"verify", "--not-after", "-", signature.getAbsolutePath(), cert.getAbsolutePath()}); SopCLI.main(new String[] {"verify", "--not-after", "-", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(verify, times(1)).notAfter(DateParser.END_OF_TIME); verify(detachedVerify, times(1)).notAfter(AbstractSopCmd.END_OF_TIME);
} }
@Test @Test
@ExpectSystemExitWithStatus(37) @ExpectSystemExitWithStatus(37)
public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
when(verify.notAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); when(detachedVerify.notAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported."));
SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
} }
@ -104,34 +103,34 @@ public class VerifyCmdTest {
public void notBefore_passedDown() throws SOPGPException.UnsupportedOption { public void notBefore_passedDown() throws SOPGPException.UnsupportedOption {
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"); Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z");
SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(verify, times(1)).notBefore(date); verify(detachedVerify, times(1)).notBefore(date);
} }
@Test @Test
public void notBefore_now() throws SOPGPException.UnsupportedOption { public void notBefore_now() throws SOPGPException.UnsupportedOption {
Date now = new Date(); Date now = new Date();
SopCLI.main(new String[] {"verify", "--not-before", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); SopCLI.main(new String[] {"verify", "--not-before", "now", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(verify, times(1)).notBefore(dateMatcher(now)); verify(detachedVerify, times(1)).notBefore(dateMatcher(now));
} }
@Test @Test
public void notBefore_dashCountsAsBeginningOfTime() throws SOPGPException.UnsupportedOption { public void notBefore_dashCountsAsBeginningOfTime() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"verify", "--not-before", "-", signature.getAbsolutePath(), cert.getAbsolutePath()}); SopCLI.main(new String[] {"verify", "--not-before", "-", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(verify, times(1)).notBefore(DateParser.BEGINNING_OF_TIME); verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME);
} }
@Test @Test
@ExpectSystemExitWithStatus(37) @ExpectSystemExitWithStatus(37)
public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
when(verify.notBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); when(detachedVerify.notBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported."));
SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
} }
@Test @Test
public void notBeforeAndNotAfterAreCalledWithDefaultValues() throws SOPGPException.UnsupportedOption { public void notBeforeAndNotAfterAreCalledWithDefaultValues() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
verify(verify, times(1)).notAfter(dateMatcher(new Date())); verify(detachedVerify, times(1)).notAfter(dateMatcher(new Date()));
verify(verify, times(1)).notBefore(DateParser.BEGINNING_OF_TIME); verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME);
} }
private static Date dateMatcher(Date date) { private static Date dateMatcher(Date date) {
@ -139,48 +138,48 @@ public class VerifyCmdTest {
} }
@Test @Test
@ExpectSystemExitWithStatus(1) @ExpectSystemExitWithStatus(61)
public void cert_fileNotFoundCausesExit1() { public void cert_fileNotFoundCausesExit61() {
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), "invalid.asc"}); SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), "invalid.asc"});
} }
@Test @Test
@ExpectSystemExitWithStatus(41) @ExpectSystemExitWithStatus(41)
public void cert_badDataCausesExit41() throws SOPGPException.BadData { public void cert_badDataCausesExit41() throws SOPGPException.BadData {
when(verify.cert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(detachedVerify.cert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
} }
@Test @Test
@ExpectSystemExitWithStatus(1) @ExpectSystemExitWithStatus(61)
public void signature_fileNotFoundCausesExit1() { public void signature_fileNotFoundCausesExit61() {
SopCLI.main(new String[] {"verify", "invalid.sig", cert.getAbsolutePath()}); SopCLI.main(new String[] {"verify", "invalid.sig", cert.getAbsolutePath()});
} }
@Test @Test
@ExpectSystemExitWithStatus(41) @ExpectSystemExitWithStatus(41)
public void signature_badDataCausesExit41() throws SOPGPException.BadData { public void signature_badDataCausesExit41() throws SOPGPException.BadData {
when(verify.signatures((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(detachedVerify.signatures((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
} }
@Test @Test
@ExpectSystemExitWithStatus(3) @ExpectSystemExitWithStatus(3)
public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
when(verify.data((InputStream) any())).thenThrow(new SOPGPException.NoSignature()); when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.NoSignature());
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
} }
@Test @Test
@ExpectSystemExitWithStatus(41) @ExpectSystemExitWithStatus(41)
public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
when(verify.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
} }
@Test @Test
public void resultIsPrintedProperly() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { public void resultIsPrintedProperly() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
when(verify.data((InputStream) any())).thenReturn(Arrays.asList( when(detachedVerify.data((InputStream) any())).thenReturn(Arrays.asList(
new Verification(UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"), new Verification(UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"),
"EB85BB5FA33A75E15E944E63F231550C4F47E38E", "EB85BB5FA33A75E15E944E63F231550C4F47E38E",
"EB85BB5FA33A75E15E944E63F231550C4F47E38E"), "EB85BB5FA33A75E15E944E63F231550C4F47E38E"),

View file

@ -13,8 +13,8 @@ import sop.operation.GenerateKey;
import sop.operation.InlineDetach; import sop.operation.InlineDetach;
import sop.operation.InlineSign; import sop.operation.InlineSign;
import sop.operation.InlineVerify; import sop.operation.InlineVerify;
import sop.operation.Sign; import sop.operation.DetachedSign;
import sop.operation.Verify; import sop.operation.DetachedVerify;
import sop.operation.Version; import sop.operation.Version;
/** /**
@ -47,19 +47,72 @@ public interface SOP {
/** /**
* Create detached signatures. * Create detached signatures.
* Customize the operation using the builder {@link Sign}. * Customize the operation using the builder {@link DetachedSign}.
*
* If you want to sign a message inline, use {@link #inlineSign()} instead.
* *
* @return builder instance * @return builder instance
*/ */
Sign sign(); default DetachedSign sign() {
return detachedSign();
}
/**
* Create detached signatures.
* Customize the operation using the builder {@link DetachedSign}.
*
* If you want to sign a message inline, use {@link #inlineSign()} instead.
*
* @return builder instance
*/
DetachedSign detachedSign();
/**
* Sign a message using inline signatures.
*
* If you need to create detached signatures, use {@link #detachedSign()} instead.
*
* @return builder instance
*/
InlineSign inlineSign();
/** /**
* Verify detached signatures. * Verify detached signatures.
* Customize the operation using the builder {@link Verify}. * Customize the operation using the builder {@link DetachedVerify}.
*
* If you need to verify an inline-signed message, use {@link #inlineVerify()} instead.
* *
* @return builder instance * @return builder instance
*/ */
Verify verify(); default DetachedVerify verify() {
return detachedVerify();
}
/**
* Verify detached signatures.
* Customize the operation using the builder {@link DetachedVerify}.
*
* If you need to verify an inline-signed message, use {@link #inlineVerify()} instead.
*
* @return builder instance
*/
DetachedVerify detachedVerify();
/**
* Verify signatures of an inline-signed message.
*
* If you need to verify detached signatures over a message, use {@link #detachedVerify()} instead.
*
* @return builder instance
*/
InlineVerify inlineVerify();
/**
* Detach signatures from an inline signed message.
*
* @return builder instance
*/
InlineDetach inlineDetach();
/** /**
* Encrypt a message. * Encrypt a message.
@ -93,9 +146,4 @@ public interface SOP {
*/ */
Dearmor dearmor(); Dearmor dearmor();
InlineDetach inlineDetach();
InlineSign inlineSign();
InlineVerify inlineVerify();
} }

View file

@ -6,6 +6,5 @@ package sop.enums;
public enum EncryptAs { public enum EncryptAs {
Binary, Binary,
Text, Text
MIME
} }

View file

@ -35,6 +35,10 @@ public abstract class SOPGPException extends RuntimeException {
super("No verifiable signature found."); super("No verifiable signature found.");
} }
public NoSignature(String errorMsg, NoSignature e) {
super(errorMsg, e);
}
@Override @Override
public int getExitCode() { public int getExitCode() {
return EXIT_CODE; return EXIT_CODE;
@ -225,6 +229,10 @@ public abstract class SOPGPException extends RuntimeException {
super(message, cause); super(message, cause);
} }
public MissingInput(String errorMsg) {
super(errorMsg);
}
@Override @Override
public int getExitCode() { public int getExitCode() {
return EXIT_CODE; return EXIT_CODE;
@ -276,6 +284,10 @@ public abstract class SOPGPException extends RuntimeException {
public static final int EXIT_CODE = 71; public static final int EXIT_CODE = 71;
public UnsupportedSpecialPrefix(String errorMsg) {
super(errorMsg);
}
@Override @Override
public int getExitCode() { public int getExitCode() {
return EXIT_CODE; return EXIT_CODE;

View file

@ -20,7 +20,7 @@ public interface AbstractSign<T> {
* *
* @return builder instance * @return builder instance
*/ */
Sign noArmor(); DetachedSign noArmor();
/** /**
* Add one or more signing keys. * Add one or more signing keys.

View file

@ -12,9 +12,9 @@ import java.util.Date;
/** /**
* Common API methods shared between verification of inline signatures ({@link InlineVerify}) * Common API methods shared between verification of inline signatures ({@link InlineVerify})
* and verification of detached signatures ({@link Verify}). * and verification of detached signatures ({@link DetachedVerify}).
* *
* @param <T> Builder type ({@link Verify}, {@link InlineVerify}) * @param <T> Builder type ({@link DetachedVerify}, {@link InlineVerify})
*/ */
public interface AbstractVerify<T> { public interface AbstractVerify<T> {

View file

@ -9,7 +9,7 @@ import sop.exception.SOPGPException;
import java.io.InputStream; import java.io.InputStream;
public interface Sign extends AbstractSign<Sign> { public interface DetachedSign extends AbstractSign<DetachedSign> {
/** /**
* Sets the signature mode. * Sets the signature mode.
@ -20,6 +20,6 @@ public interface Sign extends AbstractSign<Sign> {
* *
* @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported
*/ */
Sign mode(SignAs mode) throws SOPGPException.UnsupportedOption; DetachedSign mode(SignAs mode) throws SOPGPException.UnsupportedOption;
} }

View file

@ -12,7 +12,7 @@ import java.io.InputStream;
/** /**
* API for verifying detached signatures. * API for verifying detached signatures.
*/ */
public interface Verify extends AbstractVerify<Verify>, VerifySignatures { public interface DetachedVerify extends AbstractVerify<DetachedVerify>, VerifySignatures {
/** /**
* Provides the detached signatures. * Provides the detached signatures.

View file

@ -20,6 +20,6 @@ public interface InlineSign extends AbstractSign<InlineSign> {
* *
* @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported
*/ */
Sign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption; DetachedSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption;
} }