From a7f02d58ccdd312e344f98e2298a89611d822ffc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 29 May 2022 21:17:03 +0200 Subject: [PATCH] Wip --- .../src/main/java/sop/cli/picocli/SopCLI.java | 4 + .../cli/picocli/commands/AbstractSopCmd.java | 47 +++++++ .../sop/cli/picocli/commands/ArmorCmd.java | 9 +- .../sop/cli/picocli/commands/DearmorCmd.java | 12 +- .../sop/cli/picocli/commands/DecryptCmd.java | 66 ++++----- .../sop/cli/picocli/commands/EncryptCmd.java | 26 +++- .../cli/picocli/commands/ExtractCertCmd.java | 8 +- .../cli/picocli/commands/GenerateKeyCmd.java | 8 +- .../cli/picocli/commands/InlineDetachCmd.java | 23 ++- .../cli/picocli/commands/InlineSignCmd.java | 133 ++++++++++++++++++ .../cli/picocli/commands/InlineVerifyCmd.java | 130 +++++++++++++++++ .../sop/cli/picocli/commands/SignCmd.java | 23 +-- .../sop/cli/picocli/commands/VerifyCmd.java | 10 +- .../sop/cli/picocli/commands/VersionCmd.java | 9 +- .../test/java/sop/cli/picocli/SOPTest.java | 16 ++- sop-java/src/main/java/sop/SOP.java | 10 +- .../main/java/sop/operation/AbstractSign.java | 4 +- .../src/main/java/sop/operation/Encrypt.java | 19 +++ 18 files changed, 442 insertions(+), 115 deletions(-) create mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java create mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java create mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java index 327c0d9..1193edf 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java @@ -14,6 +14,8 @@ import sop.cli.picocli.commands.InlineDetachCmd; import sop.cli.picocli.commands.EncryptCmd; import sop.cli.picocli.commands.ExtractCertCmd; import sop.cli.picocli.commands.GenerateKeyCmd; +import sop.cli.picocli.commands.InlineSignCmd; +import sop.cli.picocli.commands.InlineVerifyCmd; import sop.cli.picocli.commands.SignCmd; import sop.cli.picocli.commands.VerifyCmd; import sop.cli.picocli.commands.VersionCmd; @@ -33,6 +35,8 @@ import sop.cli.picocli.commands.VersionCmd; GenerateKeyCmd.class, SignCmd.class, VerifyCmd.class, + InlineSignCmd.class, + InlineVerifyCmd.class, VersionCmd.class, AutoComplete.GenerateCompletion.class }, diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java new file mode 100644 index 0000000..afaf521 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands; + +import sop.exception.SOPGPException; + +import java.io.File; +import java.util.Collection; + +public abstract class AbstractSopCmd implements Runnable { + + static final String ERROR_UNSUPPORTED_OPTION = "Option '%s' is not supported."; + 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."; + + void throwIfOutputExists(File outputFile, String optionName) { + if (outputFile == null) { + return; + } + + if (outputFile.exists()) { + throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_OF_OPTION_EXISTS, outputFile.getAbsolutePath(), optionName)); + } + } + + void throwIfMissingArg(Object arg, String argName) { + if (arg == null) { + throw new SOPGPException.MissingArg(argName + " is required."); + } + } + + void throwIfEmptyParameters(Collection arg, String parmName) { + if (arg.isEmpty()) { + throw new SOPGPException.MissingArg("Parameter '" + parmName + "' is required."); + } + } + + T throwIfUnsupportedSubcommand(T subcommand, String subcommandName) { + if (subcommand == null) { + throw new SOPGPException.UnsupportedSubcommand("Command '" + subcommandName + "' is not supported."); + } + return subcommand; + } + +} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java index a015a68..7340b5c 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java @@ -17,17 +17,16 @@ import sop.operation.Armor; @CommandLine.Command(name = "armor", description = "Add ASCII Armor to standard input", exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ArmorCmd implements Runnable { +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}") ArmorLabel label; @Override public void run() { - Armor armor = SopCLI.getSop().armor(); - if (armor == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'armor' not implemented."); - } + Armor armor = throwIfUnsupportedSubcommand( + SopCLI.getSop().armor(), + "armor"); if (label != null) { try { diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java index 343b113..36d6ced 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java @@ -15,19 +15,15 @@ import sop.operation.Dearmor; @CommandLine.Command(name = "dearmor", description = "Remove ASCII Armor from standard input", exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class DearmorCmd implements Runnable { +public class DearmorCmd extends AbstractSopCmd { @Override public void run() { - Dearmor dearmor = SopCLI.getSop().dearmor(); - if (dearmor == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'dearmor' not implemented."); - } + Dearmor dearmor = throwIfUnsupportedSubcommand( + SopCLI.getSop().dearmor(), "dearmor"); try { - SopCLI.getSop() - .dearmor() - .data(System.in) + dearmor.data(System.in) .writeTo(System.out); } catch (SOPGPException.BadData e) { Print.errln("Bad data."); diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java index 87f37c4..968988d 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java @@ -30,51 +30,54 @@ import sop.util.HexUtil; @CommandLine.Command(name = "decrypt", description = "Decrypt a message from standard input", exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class DecryptCmd implements Runnable { +public class DecryptCmd extends AbstractSopCmd { - private static final String SESSION_KEY_OUT = "--session-key-out"; - private static final String VERIFY_OUT = "--verify-out"; + private static final String OPT_WITH_SESSION_KEY = "--with-session-key"; + private static final String OPT_WITH_PASSWORD = "--with-password"; + private static final String OPT_NOT_BEFORE = "--not-before"; + private static final String OPT_NOT_AFTER = "--not-after"; + private static final String OPT_SESSION_KEY_OUT = "--session-key-out"; + private static final String OPT_VERIFY_OUT = "--verify-out"; + private static final String OPT_VERIFY_WITH = "--verify-with"; + private static final String OPT_WITH_KEY_PASSWORD = "--with-key-password"; - private static final String ERROR_UNSUPPORTED_OPTION = "Option '%s' is not supported."; - private static final String ERROR_FILE_NOT_EXIST = "File '%s' does not exist."; - private static final String ERROR_OUTPUT_OF_OPTION_EXISTS = "Target %s of option %s already exists."; @CommandLine.Option( - names = {SESSION_KEY_OUT}, + names = {OPT_SESSION_KEY_OUT}, description = "Can be used to learn the session key on successful decryption", paramLabel = "SESSIONKEY") File sessionKeyOut; @CommandLine.Option( - names = {"--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", paramLabel = "SESSIONKEY") List withSessionKey = new ArrayList<>(); @CommandLine.Option( - names = {"--with-password"}, + names = {OPT_WITH_PASSWORD}, description = "Provide a password file. Enables decryption based on any \"SKESK\" packets in the \"CIPHERTEXT\"", paramLabel = "PASSWORD") List withPassword = new ArrayList<>(); - @CommandLine.Option(names = {VERIFY_OUT}, + @CommandLine.Option(names = {OPT_VERIFY_OUT}, description = "Produces signature verification status to the designated file", paramLabel = "VERIFICATIONS") File verifyOut; - @CommandLine.Option(names = {"--verify-with"}, + @CommandLine.Option(names = {OPT_VERIFY_WITH}, description = "Certificates whose signatures would be acceptable for signatures over this message", paramLabel = "CERT") List certs = new ArrayList<>(); - @CommandLine.Option(names = {"--not-before"}, + @CommandLine.Option(names = {OPT_NOT_BEFORE}, description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + "Reject signatures with a creation date not in range.\n" + "Defaults to beginning of time (\"-\").", paramLabel = "DATE") String notBefore = "-"; - @CommandLine.Option(names = {"--not-after"}, + @CommandLine.Option(names = {OPT_NOT_AFTER}, description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + "Reject signatures with a creation date not in range.\n" + "Defaults to current system time (\"now\").\n" + @@ -87,20 +90,18 @@ public class DecryptCmd implements Runnable { paramLabel = "KEY") List keys = new ArrayList<>(); - @CommandLine.Option(names = "--with-key-password", + @CommandLine.Option(names = {OPT_WITH_KEY_PASSWORD}, description = "Provide indirect file type pointing at passphrase(s) for secret key(s)", paramLabel = "PASSWORD") List withKeyPassword = new ArrayList<>(); @Override public void run() { - throwIfOutputExists(verifyOut, VERIFY_OUT); - throwIfOutputExists(sessionKeyOut, SESSION_KEY_OUT); + Decrypt decrypt = throwIfUnsupportedSubcommand( + SopCLI.getSop().decrypt(), "decrypt"); - Decrypt decrypt = SopCLI.getSop().decrypt(); - if (decrypt == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'decrypt' not implemented."); - } + throwIfOutputExists(verifyOut, OPT_VERIFY_OUT); + throwIfOutputExists(sessionKeyOut, OPT_SESSION_KEY_OUT); setNotAfter(notAfter, decrypt); setNotBefore(notBefore, decrypt); @@ -112,7 +113,7 @@ public class DecryptCmd implements Runnable { if (verifyOut != null && certs.isEmpty()) { String errorMessage = "Option %s is requested, but no option %s was provided."; - throw new SOPGPException.IncompleteVerification(String.format(errorMessage, VERIFY_OUT, "--verify-with")); + throw new SOPGPException.IncompleteVerification(String.format(errorMessage, OPT_VERIFY_OUT, OPT_VERIFY_WITH)); } try { @@ -127,16 +128,6 @@ public class DecryptCmd implements Runnable { } } - private void throwIfOutputExists(File outputFile, String optionName) { - if (outputFile == null) { - return; - } - - if (outputFile.exists()) { - throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_OF_OPTION_EXISTS, outputFile.getAbsolutePath(), optionName)); - } - } - private void writeVerifyOut(DecryptionResult result) throws IOException { if (verifyOut != null) { FileUtil.createNewFileOrThrow(verifyOut); @@ -158,7 +149,8 @@ public class DecryptCmd implements Runnable { try (FileOutputStream outputStream = new FileOutputStream(sessionKeyOut)) { if (!result.getSessionKey().isPresent()) { - throw new SOPGPException.UnsupportedOption("Session key not extracted. Possibly the feature --session-key-out is not supported."); + String errorMsg = "Session key not extracted. Possibly the feature %s is not supported."; + throw new SOPGPException.UnsupportedOption(String.format(errorMsg, OPT_SESSION_KEY_OUT)); } else { SessionKey sessionKey = result.getSessionKey().get(); outputStream.write(sessionKey.getAlgorithm()); @@ -217,7 +209,7 @@ public class DecryptCmd implements Runnable { try { decrypt.withSessionKey(new SessionKey(algorithm, key)); } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-session-key"), unsupportedOption); + throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, OPT_WITH_SESSION_KEY), unsupportedOption); } } } @@ -228,7 +220,7 @@ public class DecryptCmd implements Runnable { String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(passwordFile)); decrypt.withPassword(password); } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-password"), unsupportedOption); + throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, OPT_WITH_PASSWORD), unsupportedOption); } catch (IOException e) { throw new RuntimeException(e); } @@ -241,7 +233,7 @@ public class DecryptCmd implements Runnable { String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(passwordFile)); decrypt.withKeyPassword(password); } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-key-password"), unsupportedOption); + throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, OPT_WITH_KEY_PASSWORD), unsupportedOption); } catch (IOException e) { throw new RuntimeException(e); } @@ -253,7 +245,7 @@ public class DecryptCmd implements Runnable { try { decrypt.verifyNotAfter(notAfterDate); } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-after"), unsupportedOption); + throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, OPT_NOT_AFTER), unsupportedOption); } } @@ -262,7 +254,7 @@ public class DecryptCmd implements Runnable { try { decrypt.verifyNotBefore(notBeforeDate); } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-before"), unsupportedOption); + throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, OPT_NOT_BEFORE), unsupportedOption); } } } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java index 8d26742..1535f99 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java @@ -22,7 +22,7 @@ import sop.operation.Encrypt; @CommandLine.Command(name = "encrypt", description = "Encrypt a message from standard input", exitCodeOnInvalidInput = 37) -public class EncryptCmd implements Runnable { +public class EncryptCmd extends AbstractSopCmd { @CommandLine.Option(names = "--no-armor", description = "ASCII armor the output", @@ -31,7 +31,7 @@ public class EncryptCmd implements Runnable { @CommandLine.Option(names = {"--as"}, description = "Type of the input data. Defaults to 'binary'", - paramLabel = "{binary|text|mime}") + paramLabel = "{binary|text}") EncryptAs type; @CommandLine.Option(names = "--with-password", @@ -44,6 +44,11 @@ public class EncryptCmd implements Runnable { paramLabel = "KEY") List signWith = new ArrayList<>(); + @CommandLine.Option(names = "--with-key-password", + description = "Provide indirect file type pointing at passphrase(s) for secret key(s)", + paramLabel = "PASSWORD") + List withKeyPassword = new ArrayList<>(); + @CommandLine.Parameters(description = "Certificates the message gets encrypted to", index = "0..*", paramLabel = "CERTS") @@ -51,10 +56,8 @@ public class EncryptCmd implements Runnable { @Override public void run() { - Encrypt encrypt = SopCLI.getSop().encrypt(); - if (encrypt == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'encrypt' not implemented."); - } + Encrypt encrypt = throwIfUnsupportedSubcommand( + SopCLI.getSop().encrypt(), "encrypt"); if (type != null) { try { @@ -79,6 +82,17 @@ public class EncryptCmd implements Runnable { } } + for (String passwordFileName : withKeyPassword) { + try { + String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(passwordFileName)); + encrypt.withKeyPassword(password); + } catch (SOPGPException.UnsupportedOption unsupportedOption) { + throw new SOPGPException.UnsupportedOption("Unsupported option '--with-key-password'.", unsupportedOption); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + for (File keyFile : signWith) { try (FileInputStream keyIn = new FileInputStream(keyFile)) { encrypt.signWith(keyIn); diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java index f455933..8502d19 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java @@ -15,7 +15,7 @@ import sop.operation.ExtractCert; @CommandLine.Command(name = "extract-cert", description = "Extract a public key certificate from a secret key from standard input", exitCodeOnInvalidInput = 37) -public class ExtractCertCmd implements Runnable { +public class ExtractCertCmd extends AbstractSopCmd { @CommandLine.Option(names = "--no-armor", description = "ASCII armor the output", @@ -24,10 +24,8 @@ public class ExtractCertCmd implements Runnable { @Override public void run() { - ExtractCert extractCert = SopCLI.getSop().extractCert(); - if (extractCert == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'extract-cert' not implemented."); - } + ExtractCert extractCert = throwIfUnsupportedSubcommand( + SopCLI.getSop().extractCert(), "extract-cert"); if (!armor) { extractCert.noArmor(); diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java index 56c0f1b..06df864 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java @@ -19,7 +19,7 @@ import sop.operation.GenerateKey; @CommandLine.Command(name = "generate-key", description = "Generate a secret key", exitCodeOnInvalidInput = 37) -public class GenerateKeyCmd implements Runnable { +public class GenerateKeyCmd extends AbstractSopCmd { @CommandLine.Option(names = "--no-armor", description = "ASCII armor the output", @@ -36,10 +36,8 @@ public class GenerateKeyCmd implements Runnable { @Override public void run() { - GenerateKey generateKey = SopCLI.getSop().generateKey(); - if (generateKey == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'generate-key' not implemented."); - } + GenerateKey generateKey = throwIfUnsupportedSubcommand( + SopCLI.getSop().generateKey(), "generate-key"); for (String userId : userId) { generateKey.userId(userId); diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java index 3d4a8bb..be7b85c 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java @@ -14,10 +14,10 @@ import sop.cli.picocli.SopCLI; import sop.exception.SOPGPException; import sop.operation.InlineDetach; -@CommandLine.Command(name = "detach-inband-signature-and-message", +@CommandLine.Command(name = "inline-detach", description = "Split a clearsigned message", exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class InlineDetachCmd implements Runnable { +public class InlineDetachCmd extends AbstractSopCmd { @CommandLine.Option( names = {"--signatures-out"}, @@ -32,25 +32,20 @@ public class InlineDetachCmd implements Runnable { @Override public void run() { - InlineDetach detach = SopCLI.getSop().detachInbandSignatureAndMessage(); - if (detach == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'detach-inband-signature-and-message' not implemented."); - } + InlineDetach inlineDetach = throwIfUnsupportedSubcommand( + SopCLI.getSop().inlineDetach(), "inline-detach"); - if (signaturesOut == null) { - throw new SOPGPException.MissingArg("--signatures-out is required."); - } + throwIfOutputExists(signaturesOut, "--signatures-out"); + throwIfMissingArg(signaturesOut, "--signatures-out"); if (!armor) { - detach.noArmor(); + inlineDetach.noArmor(); } try { - Signatures signatures = detach + Signatures signatures = inlineDetach .message(System.in).writeTo(System.out); - if (!signaturesOut.createNewFile()) { - throw new SOPGPException.OutputExists("Destination of --signatures-out already exists."); - } + signaturesOut.createNewFile(); signatures.writeTo(new FileOutputStream(signaturesOut)); } catch (IOException e) { throw new RuntimeException(e); diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java new file mode 100644 index 0000000..b7fafe6 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands; + +import picocli.CommandLine; +import sop.MicAlg; +import sop.ReadyWithResult; +import sop.SigningResult; +import sop.cli.picocli.FileUtil; +import sop.cli.picocli.Print; +import sop.cli.picocli.SopCLI; +import sop.enums.InlineSignAs; +import sop.exception.SOPGPException; +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.util.ArrayList; +import java.util.List; + +@CommandLine.Command(name = "inline-sign", + description = "Create an inline-signed message from data on standard input", + exitCodeOnInvalidInput = 37) +public class InlineSignCmd extends AbstractSopCmd { + + @CommandLine.Option(names = "--no-armor", + description = "ASCII armor the output", + negatable = 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.", + paramLabel = "{binary|text|cleartextsigned}") + InlineSignAs type; + + @CommandLine.Parameters(description = "Secret keys used for signing", + paramLabel = "KEYS") + List secretKeyFile = new ArrayList<>(); + + @CommandLine.Option(names = "--with-key-password", description = "Password(s) to unlock the secret key(s) with", + paramLabel = "PASSWORD") + List 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)", + paramLabel = "MICALG") + File micAlgOut; + + @Override + public void run() { + InlineSign inlineSign = throwIfUnsupportedSubcommand( + SopCLI.getSop().inlineSign(), "inline-sign"); + + throwIfOutputExists(micAlgOut, "--micalg-out"); + + if (type != null) { + try { + inlineSign.mode(type); + } catch (SOPGPException.UnsupportedOption unsupportedOption) { + Print.errln("Unsupported option '--as'"); + Print.trace(unsupportedOption); + System.exit(unsupportedOption.getExitCode()); + } + } + + if (secretKeyFile.isEmpty()) { + Print.errln("Missing required parameter 'KEYS'."); + System.exit(19); + } + + for (String passwordFile : withKeyPassword) { + try { + String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(passwordFile)); + inlineSign.withKeyPassword(password); + } catch (SOPGPException.UnsupportedOption unsupportedOption) { + throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-key-password"), unsupportedOption); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + for (File keyFile : secretKeyFile) { + try (FileInputStream keyIn = new FileInputStream(keyFile)) { + inlineSign.key(keyIn); + } catch (FileNotFoundException e) { + Print.errln("File " + keyFile.getAbsolutePath() + " does not exist."); + Print.trace(e); + System.exit(1); + } catch (IOException e) { + Print.errln("Cannot access file " + keyFile.getAbsolutePath()); + Print.trace(e); + System.exit(1); + } catch (SOPGPException.KeyIsProtected e) { + Print.errln("Key " + keyFile.getName() + " is password protected."); + Print.trace(e); + System.exit(1); + } catch (SOPGPException.BadData badData) { + Print.errln("Bad data in key file " + keyFile.getAbsolutePath() + ":"); + Print.trace(badData); + System.exit(badData.getExitCode()); + } + } + + if (!armor) { + inlineSign.noArmor(); + } + + try { + ReadyWithResult ready = inlineSign.data(System.in); + SigningResult result = ready.writeTo(System.out); + + MicAlg micAlg = result.getMicAlg(); + if (micAlgOut != null) { + // Write micalg out + micAlgOut.createNewFile(); + FileOutputStream micAlgOutStream = new FileOutputStream(micAlgOut); + micAlg.writeTo(micAlgOutStream); + micAlgOutStream.close(); + } + } catch (IOException e) { + Print.errln("IO Error."); + 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()); + } + } +} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java new file mode 100644 index 0000000..4d22dbf --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands; + +import picocli.CommandLine; +import sop.ReadyWithResult; +import sop.Verification; +import sop.cli.picocli.DateParser; +import sop.cli.picocli.Print; +import sop.cli.picocli.SopCLI; +import sop.exception.SOPGPException; +import sop.operation.InlineVerify; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +@CommandLine.Command(name = "inline-verify", + description = "Verify inline-signed data from standard input", + exitCodeOnInvalidInput = 37) +public class InlineVerifyCmd extends AbstractSopCmd { + + @CommandLine.Parameters(arity = "1..*", + description = "Public key certificates", + paramLabel = "CERT") + List certificates = new ArrayList<>(); + + @CommandLine.Option(names = {"--not-before"}, + description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + + "Reject signatures with a creation date not in range.\n" + + "Defaults to beginning of time (\"-\").", + paramLabel = "DATE") + String notBefore = "-"; + + @CommandLine.Option(names = {"--not-after"}, + description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" + + "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") + String notAfter = "now"; + + @CommandLine.Option(names = "--verifications-out", + description = "File to write details over successful verifications to") + File verificationsOut; + + @Override + public void run() { + InlineVerify inlineVerify = throwIfUnsupportedSubcommand( + SopCLI.getSop().inlineVerify(), "inline-verify"); + + throwIfOutputExists(verificationsOut, "--verifications-out"); + + if (notAfter != null) { + try { + inlineVerify.notAfter(DateParser.parseNotAfter(notAfter)); + } catch (SOPGPException.UnsupportedOption unsupportedOption) { + Print.errln("Unsupported option '--not-after'."); + Print.trace(unsupportedOption); + System.exit(unsupportedOption.getExitCode()); + } + } + if (notBefore != null) { + try { + inlineVerify.notBefore(DateParser.parseNotBefore(notBefore)); + } catch (SOPGPException.UnsupportedOption unsupportedOption) { + Print.errln("Unsupported option '--not-before'."); + Print.trace(unsupportedOption); + System.exit(unsupportedOption.getExitCode()); + } + } + + for (File certFile : certificates) { + try (FileInputStream certIn = new FileInputStream(certFile)) { + inlineVerify.cert(certIn); + } catch (FileNotFoundException fileNotFoundException) { + Print.errln("Certificate file " + certFile.getAbsolutePath() + " not found."); + + Print.trace(fileNotFoundException); + System.exit(1); + } catch (IOException ioException) { + Print.errln("IO Error."); + Print.trace(ioException); + System.exit(1); + } catch (SOPGPException.BadData badData) { + Print.errln("Certificate file " + certFile.getAbsolutePath() + " appears to not contain a valid OpenPGP certificate."); + Print.trace(badData); + System.exit(badData.getExitCode()); + } + } + + List verifications = null; + try { + ReadyWithResult> ready = inlineVerify.data(System.in); + verifications = ready.writeTo(System.out); + } catch (SOPGPException.NoSignature e) { + Print.errln("No verifiable signature found."); + Print.trace(e); + System.exit(e.getExitCode()); + } catch (IOException ioException) { + Print.errln("IO Error."); + Print.trace(ioException); + System.exit(1); + } catch (SOPGPException.BadData badData) { + Print.errln("Standard Input appears not to contain a valid OpenPGP message."); + Print.trace(badData); + System.exit(badData.getExitCode()); + } + + if (verificationsOut != null) { + try { + verificationsOut.createNewFile(); + PrintWriter pw = new PrintWriter(verificationsOut); + for (Verification verification : verifications) { + // CHECKSTYLE:OFF + pw.println(verification); + // CHECKSTYLE:ON + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java index 65ed6cd..3843d13 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java @@ -21,15 +21,12 @@ import sop.cli.picocli.Print; import sop.cli.picocli.SopCLI; import sop.enums.SignAs; import sop.exception.SOPGPException; -import sop.operation.Decrypt; import sop.operation.Sign; @CommandLine.Command(name = "sign", description = "Create a detached signature on the data from standard input", exitCodeOnInvalidInput = 37) -public class SignCmd implements Runnable { - - private static final String ERROR_UNSUPPORTED_OPTION = "Option '%s' is not supported."; +public class SignCmd extends AbstractSopCmd { @CommandLine.Option(names = "--no-armor", description = "ASCII armor the output", @@ -54,10 +51,11 @@ public class SignCmd implements Runnable { @Override public void run() { - Sign sign = SopCLI.getSop().sign(); - if (sign == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'sign' not implemented."); - } + Sign sign = throwIfUnsupportedSubcommand( + SopCLI.getSop().sign(), "sign"); + + throwIfOutputExists(micAlgOut, "--micalg-out"); + throwIfEmptyParameters(secretKeyFile, "KEYS"); if (type != null) { try { @@ -69,15 +67,6 @@ public class SignCmd implements Runnable { } } - if (micAlgOut != null && micAlgOut.exists()) { - throw new SOPGPException.OutputExists(String.format("Target %s of option %s already exists.", micAlgOut.getAbsolutePath(), "--micalg-out")); - } - - if (secretKeyFile.isEmpty()) { - Print.errln("Missing required parameter 'KEYS'."); - System.exit(19); - } - for (String passwordFile : withKeyPassword) { try { String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(passwordFile)); diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java index 2702b4b..0d4b596 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java @@ -22,14 +22,14 @@ import sop.operation.Verify; @CommandLine.Command(name = "verify", description = "Verify a detached signature over the data from standard input", exitCodeOnInvalidInput = 37) -public class VerifyCmd implements Runnable { +public class VerifyCmd extends AbstractSopCmd { @CommandLine.Parameters(index = "0", description = "Detached signature", paramLabel = "SIGNATURE") File signature; - @CommandLine.Parameters(index = "1..*", + @CommandLine.Parameters(index = "0..*", arity = "1..*", description = "Public key certificates", paramLabel = "CERT") @@ -52,10 +52,8 @@ public class VerifyCmd implements Runnable { @Override public void run() { - Verify verify = SopCLI.getSop().verify(); - if (verify == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'verify' not implemented."); - } + Verify verify = throwIfUnsupportedSubcommand( + SopCLI.getSop().verify(), "verify"); if (notAfter != null) { try { diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java index 4a31919..64d69ca 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java @@ -7,12 +7,11 @@ package sop.cli.picocli.commands; import picocli.CommandLine; import sop.cli.picocli.Print; import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; import sop.operation.Version; @CommandLine.Command(name = "version", description = "Display version information about the tool", exitCodeOnInvalidInput = 37) -public class VersionCmd implements Runnable { +public class VersionCmd extends AbstractSopCmd { @CommandLine.ArgGroup() Exclusive exclusive; @@ -29,10 +28,8 @@ public class VersionCmd implements Runnable { @Override public void run() { - Version version = SopCLI.getSop().version(); - if (version == null) { - throw new SOPGPException.UnsupportedSubcommand("Command 'version' not implemented."); - } + Version version = throwIfUnsupportedSubcommand( + SopCLI.getSop().version(), "version"); if (exclusive == null) { Print.outln(version.getName() + " " + version.getVersion()); diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java index 127f853..b71dc59 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java @@ -21,6 +21,8 @@ import sop.operation.InlineDetach; import sop.operation.Encrypt; import sop.operation.ExtractCert; import sop.operation.GenerateKey; +import sop.operation.InlineSign; +import sop.operation.InlineVerify; import sop.operation.Sign; import sop.operation.Verify; import sop.operation.Version; @@ -93,7 +95,17 @@ public class SOPTest { } @Override - public InlineDetach detachInbandSignatureAndMessage() { + public InlineDetach inlineDetach() { + return null; + } + + @Override + public InlineSign inlineSign() { + return null; + } + + @Override + public InlineVerify inlineVerify() { return null; } }; @@ -103,7 +115,7 @@ public class SOPTest { commands.add(new String[] {"armor"}); commands.add(new String[] {"dearmor"}); commands.add(new String[] {"decrypt"}); - commands.add(new String[] {"detach-inband-signature-and-message"}); + commands.add(new String[] {"inline-detach", "--signatures-out", "sigs.asc"}); commands.add(new String[] {"encrypt"}); commands.add(new String[] {"extract-cert"}); commands.add(new String[] {"generate-key"}); diff --git a/sop-java/src/main/java/sop/SOP.java b/sop-java/src/main/java/sop/SOP.java index f2f5392..9068bab 100644 --- a/sop-java/src/main/java/sop/SOP.java +++ b/sop-java/src/main/java/sop/SOP.java @@ -7,10 +7,12 @@ package sop; import sop.operation.Armor; import sop.operation.Dearmor; import sop.operation.Decrypt; -import sop.operation.InlineDetach; import sop.operation.Encrypt; import sop.operation.ExtractCert; import sop.operation.GenerateKey; +import sop.operation.InlineDetach; +import sop.operation.InlineSign; +import sop.operation.InlineVerify; import sop.operation.Sign; import sop.operation.Verify; import sop.operation.Version; @@ -91,5 +93,9 @@ public interface SOP { */ Dearmor dearmor(); - InlineDetach detachInbandSignatureAndMessage(); + InlineDetach inlineDetach(); + + InlineSign inlineSign(); + + InlineVerify inlineVerify(); } diff --git a/sop-java/src/main/java/sop/operation/AbstractSign.java b/sop-java/src/main/java/sop/operation/AbstractSign.java index 4fd0b8a..fe28c47 100644 --- a/sop-java/src/main/java/sop/operation/AbstractSign.java +++ b/sop-java/src/main/java/sop/operation/AbstractSign.java @@ -49,7 +49,7 @@ public interface AbstractSign { } /** - * Provide the decryption password for the secret key. + * Provide the password for the secret key used for signing. * * @param password password * @return builder instance @@ -59,7 +59,7 @@ public interface AbstractSign { } /** - * Provide the decryption password for the secret key. + * Provide the password for the secret key used for signing. * * @param password password * @return builder instance diff --git a/sop-java/src/main/java/sop/operation/Encrypt.java b/sop-java/src/main/java/sop/operation/Encrypt.java index 5375ac3..b82419c 100644 --- a/sop-java/src/main/java/sop/operation/Encrypt.java +++ b/sop-java/src/main/java/sop/operation/Encrypt.java @@ -7,6 +7,7 @@ package sop.operation; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.Charset; import sop.Ready; import sop.enums.EncryptAs; @@ -68,6 +69,24 @@ public interface Encrypt { return signWith(new ByteArrayInputStream(key)); } + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + */ + default Encrypt withKeyPassword(String password) { + return withKeyPassword(password.getBytes(Charset.forName("UTF8"))); + } + + /** + * Provide the password for the secret key used for sigining. + * + * @param password password + * @return builder instance + */ + Encrypt withKeyPassword(byte[] password); + /** * Encrypt with the given password. *