mirror of
https://codeberg.org/PGPainless/sop-java.git
synced 2024-12-26 14:37:56 +01:00
Wip
This commit is contained in:
parent
8184c30617
commit
a7f02d58cc
18 changed files with 442 additions and 115 deletions
|
@ -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
|
||||
},
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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> T throwIfUnsupportedSubcommand(T subcommand, String subcommandName) {
|
||||
if (subcommand == null) {
|
||||
throw new SOPGPException.UnsupportedSubcommand("Command '" + subcommandName + "' is not supported.");
|
||||
}
|
||||
return subcommand;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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<String> 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<String> 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<File> 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<File> 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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<File> 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<String> 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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.cli.picocli.commands;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import sop.MicAlg;
|
||||
import sop.ReadyWithResult;
|
||||
import sop.SigningResult;
|
||||
import sop.cli.picocli.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<File> secretKeyFile = new ArrayList<>();
|
||||
|
||||
@CommandLine.Option(names = "--with-key-password", description = "Password(s) to unlock the secret key(s) with",
|
||||
paramLabel = "PASSWORD")
|
||||
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)",
|
||||
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<SigningResult> 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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.cli.picocli.commands;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import sop.ReadyWithResult;
|
||||
import sop.Verification;
|
||||
import sop.cli.picocli.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<File> 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<Verification> verifications = null;
|
||||
try {
|
||||
ReadyWithResult<List<Verification>> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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"});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ public interface AbstractSign<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Provide the decryption password for the secret key.
|
||||
* Provide the password for the secret key used for signing.
|
||||
*
|
||||
* @param password password
|
||||
* @return builder instance
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
Loading…
Reference in a new issue