From 173bc55eb9a6110894d845e5eba8ef9ce5784c3e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:41:44 +0100 Subject: [PATCH 001/110] Fix javadoc reference --- external-sop/src/main/kotlin/sop/external/ExternalSOP.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt index 3a0ef52..27c93ae 100644 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -16,7 +16,7 @@ import sop.external.operation.* import sop.operation.* /** - * Implementation of the {@link SOP} API using an external SOP binary. + * Implementation of the [SOP] API using an external SOP binary. * * Instantiate an [ExternalSOP] object for the given binary and the given [TempDirProvider] using * empty environment variables. From a0e7356757780e96cd42a337d46011787f71eb7b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:42:54 +0100 Subject: [PATCH 002/110] Replace assumeTrue(false) with explicit TestAbortedException --- .../java/sop/testsuite/operation/VersionTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java index 73ba571..0b19d20 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.opentest4j.TestAbortedException; import sop.SOP; import java.util.stream.Stream; @@ -15,7 +16,6 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeTrue; @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") public class VersionTest extends AbstractSOPTest { @@ -59,7 +59,7 @@ public class VersionTest extends AbstractSOPTest { try { sop.version().getSopSpecVersion(); } catch (RuntimeException e) { - assumeTrue(false); // SOP backend does not support this operation yet + throw new TestAbortedException("SOP backend does not support 'version --sop-spec' yet."); } String sopSpec = sop.version().getSopSpecVersion(); From 7b04275625de94b084da6b75102fb7b7141b9795 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:43:39 +0100 Subject: [PATCH 003/110] Add test ckecking that BadData is thrown if KEYS is passed for CERTS --- .../sop/testsuite/operation/EncryptDecryptTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java index 51c117a..df824ca 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java @@ -327,4 +327,15 @@ public class EncryptDecryptTest extends AbstractSOPTest { .toByteArrayAndResult() .getBytes()); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void passingSecretKeysForPublicKeysFails(SOP sop) { + assertThrows(SOPGPException.BadData.class, () -> + sop.encrypt() + .withCert(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) + .toByteArrayAndResult() + .getBytes()); + } } From 34a05e96a103f392fff06dc9d901ebe611d32db9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:45:14 +0100 Subject: [PATCH 004/110] Move signature verification operations to sopv interface subset --- .../main/kotlin/sop/external/ExternalSOPV.kt | 53 ++++++++++ .../sop/external/operation/VersionExternal.kt | 4 + .../main/kotlin/sop/cli/picocli/SopVCLI.kt | 98 +++++++++++++++++++ .../sop/cli/picocli/commands/VersionCmd.kt | 6 ++ .../src/main/resources/msg_sop.properties | 2 + .../src/main/resources/msg_sop_de.properties | 2 + sop-java/src/main/kotlin/sop/SOP.kt | 26 +---- sop-java/src/main/kotlin/sop/SOPV.kt | 34 +++++++ .../src/main/kotlin/sop/operation/Version.kt | 10 ++ .../sop/testsuite/operation/VersionTest.java | 14 +++ 10 files changed, 224 insertions(+), 25 deletions(-) create mode 100644 external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt create mode 100644 sop-java/src/main/kotlin/sop/SOPV.kt diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt new file mode 100644 index 0000000..f22f947 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external + +import java.nio.file.Files +import java.util.* +import sop.SOPV +import sop.external.ExternalSOP.TempDirProvider +import sop.external.operation.DetachedVerifyExternal +import sop.external.operation.InlineVerifyExternal +import sop.external.operation.VersionExternal +import sop.operation.DetachedVerify +import sop.operation.InlineVerify +import sop.operation.Version + +/** + * Implementation of the [SOPV] API subset using an external sopv/sop binary. + * + * Instantiate an [ExternalSOPV] object for the given binary and the given [TempDirProvider] using + * empty environment variables. + * + * @param binaryName name / path of the sopv binary + * @param tempDirProvider custom tempDirProvider + */ +class ExternalSOPV( + private val binaryName: String, + private val properties: Properties = Properties(), + private val tempDirProvider: TempDirProvider = defaultTempDirProvider() +) : SOPV { + + override fun version(): Version = VersionExternal(binaryName, properties) + + override fun detachedVerify(): DetachedVerify = DetachedVerifyExternal(binaryName, properties) + + override fun inlineVerify(): InlineVerify = + InlineVerifyExternal(binaryName, properties, tempDirProvider) + + companion object { + + /** + * Default implementation of the [TempDirProvider] which stores temporary files in the + * systems temp dir ([Files.createTempDirectory]). + * + * @return default implementation + */ + @JvmStatic + fun defaultTempDirProvider(): TempDirProvider { + return TempDirProvider { Files.createTempDirectory("ext-sopv").toFile() } + } + } +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt index 7e13fc1..728f3b6 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt @@ -68,6 +68,10 @@ class VersionExternal(binary: String, environment: Properties) : Version { return null } + override fun getSopVVersion(): String { + return executeForLines(commandList.plus("--sopv")) + } + override fun getSopSpecVersion(): String { return executeForLines(commandList.plus("--sop-spec")) } diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt new file mode 100644 index 0000000..9a8b4b4 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli + +import java.util.* +import kotlin.system.exitProcess +import picocli.AutoComplete +import picocli.CommandLine +import sop.SOPV +import sop.cli.picocli.commands.* +import sop.exception.SOPGPException + +@CommandLine.Command( + name = "sopv", + resourceBundle = "msg_sop", + exitCodeOnInvalidInput = SOPGPException.UnsupportedSubcommand.EXIT_CODE, + subcommands = + [ + // Meta subcommands + VersionCmd::class, + // signature verification subcommands + VerifyCmd::class, + InlineVerifyCmd::class, + // misc + CommandLine.HelpCommand::class, + AutoComplete.GenerateCompletion::class]) +class SopVCLI { + + companion object { + @JvmStatic private var sopvInstance: SOPV? = null + + @JvmStatic + fun getSopV(): SOPV = + checkNotNull(sopvInstance) { cliMsg.getString("sop.error.runtime.no_backend_set") } + + @JvmStatic + fun setSopVInstance(sopv: SOPV?) { + sopvInstance = sopv + } + + @JvmField var cliMsg: ResourceBundle = ResourceBundle.getBundle("msg_sop") + + @JvmField var EXECUTABLE_NAME = "sopv" + + @JvmField + @CommandLine.Option(names = ["--stacktrace"], scope = CommandLine.ScopeType.INHERIT) + var stacktrace = false + + @JvmStatic + fun main(vararg args: String) { + val exitCode = execute(*args) + if (exitCode != 0) { + exitProcess(exitCode) + } + } + + @JvmStatic + fun execute(vararg args: String): Int { + // Set locale + CommandLine(InitLocale()).parseArgs(*args) + + // Re-set bundle with updated locale + cliMsg = ResourceBundle.getBundle("msg_sop") + + return CommandLine(SopVCLI::class.java) + .apply { + // explicitly set help command resource bundle + subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help")) + // Hide generate-completion command + subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true) + // overwrite executable name + commandName = EXECUTABLE_NAME + // setup exception handling + executionExceptionHandler = SOPExecutionExceptionHandler() + exitCodeExceptionMapper = SOPExceptionExitCodeMapper() + isCaseInsensitiveEnumValuesAllowed = true + } + .execute(*args) + } + } + + /** + * Control the locale. + * + * @see Picocli Readme + */ + @CommandLine.Command + class InitLocale { + @CommandLine.Option(names = ["-l", "--locale"], descriptionKey = "sop.locale") + fun setLocale(locale: String) = Locale.setDefault(Locale(locale)) + + @CommandLine.Unmatched + var remainder: MutableList = + mutableListOf() // ignore any other parameters and options in the first parsing phase + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt index 75197fe..8b1936a 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt @@ -22,6 +22,7 @@ class VersionCmd : AbstractSopCmd() { @Option(names = ["--extended"]) var extended: Boolean = false @Option(names = ["--backend"]) var backend: Boolean = false @Option(names = ["--sop-spec"]) var sopSpec: Boolean = false + @Option(names = ["--sopv"]) var sopv: Boolean = false } override fun run() { @@ -47,5 +48,10 @@ class VersionCmd : AbstractSopCmd() { println(version.getSopSpecVersion()) return } + + if (exclusive!!.sopv) { + println(version.getSopVVersion()) + return + } } } diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 52c5368..7979eb3 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -2,7 +2,9 @@ # # SPDX-License-Identifier: Apache-2.0 sop.name=sop +sopv.name=sopv usage.header=Stateless OpenPGP Protocol +sopv.usage.header=Stateless OpenPGP Protocol - Signature Verification Interface Subset locale=Locale for description texts # Generic diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 5900f39..40a316d 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -2,7 +2,9 @@ # # SPDX-License-Identifier: Apache-2.0 sop.name=sop +sopv.name=sopv usage.header=Stateless OpenPGP Protocol +sopv.usage.header=Stateless OpenPGP Protocol - Signature Verification Interface Subset locale=Gebietsschema für Beschreibungstexte # Generic diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index e01763a..7fdd414 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -9,16 +9,13 @@ import sop.operation.ChangeKeyPassword import sop.operation.Dearmor import sop.operation.Decrypt import sop.operation.DetachedSign -import sop.operation.DetachedVerify 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.ListProfiles import sop.operation.RevokeKey -import sop.operation.Version /** * Stateless OpenPGP Interface. This class provides a stateless interface to various OpenPGP related @@ -26,10 +23,7 @@ import sop.operation.Version * intended for reuse. If you for example need to generate multiple keys, make a dedicated call to * [generateKey] once per key generation. */ -interface SOP { - - /** Get information about the implementations name and version. */ - fun version(): Version +interface SOP : SOPV { /** Generate a secret key. */ fun generateKey(): GenerateKey @@ -53,24 +47,6 @@ interface SOP { */ fun inlineSign(): InlineSign - /** - * Verify detached signatures. If you need to verify an inline-signed message, use - * [inlineVerify] instead. - */ - fun verify(): DetachedVerify = detachedVerify() - - /** - * Verify detached signatures. If you need to verify an inline-signed message, use - * [inlineVerify] instead. - */ - fun detachedVerify(): DetachedVerify - - /** - * Verify signatures of an inline-signed message. If you need to verify detached signatures over - * a message, use [detachedVerify] instead. - */ - fun inlineVerify(): InlineVerify - /** Detach signatures from an inline signed message. */ fun inlineDetach(): InlineDetach diff --git a/sop-java/src/main/kotlin/sop/SOPV.kt b/sop-java/src/main/kotlin/sop/SOPV.kt new file mode 100644 index 0000000..d331559 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/SOPV.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import sop.operation.DetachedVerify +import sop.operation.InlineVerify +import sop.operation.Version + +/** Subset of [SOP] implementing only OpenPGP signature verification. */ +interface SOPV { + + /** Get information about the implementations name and version. */ + fun version(): Version + + /** + * Verify detached signatures. If you need to verify an inline-signed message, use + * [inlineVerify] instead. + */ + fun verify(): DetachedVerify = detachedVerify() + + /** + * Verify detached signatures. If you need to verify an inline-signed message, use + * [inlineVerify] instead. + */ + fun detachedVerify(): DetachedVerify + + /** + * Verify signatures of an inline-signed message. If you need to verify detached signatures over + * a message, use [detachedVerify] instead. + */ + fun inlineVerify(): InlineVerify +} diff --git a/sop-java/src/main/kotlin/sop/operation/Version.kt b/sop-java/src/main/kotlin/sop/operation/Version.kt index 9b3bd8a..5f26491 100644 --- a/sop-java/src/main/kotlin/sop/operation/Version.kt +++ b/sop-java/src/main/kotlin/sop/operation/Version.kt @@ -4,6 +4,9 @@ package sop.operation +import kotlin.jvm.Throws +import sop.exception.SOPGPException + interface Version { /** @@ -97,4 +100,11 @@ interface Version { * @return remarks or null */ fun getSopSpecImplementationRemarks(): String? + + /** + * Return the single-line SEMVER version of the sopv interface subset it provides complete + * coverage of. If the implementation does not provide complete coverage for any sopv interface, + * this method throws an [SOPGPException.UnsupportedOption] instead. + */ + @Throws(SOPGPException.UnsupportedOption::class) fun getSopVVersion(): String } diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java index 0b19d20..f836935 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.opentest4j.TestAbortedException; import sop.SOP; +import sop.exception.SOPGPException; import java.util.stream.Stream; @@ -72,4 +73,17 @@ public class VersionTest extends AbstractSOPTest { int sopRevision = sop.version().getSopSpecRevisionNumber(); assertTrue(sop.version().getSopSpecRevisionName().endsWith("" + sopRevision)); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void sopVVersionTest(SOP sop) { + try { + sop.version().getSopVVersion(); + } catch (SOPGPException.UnsupportedOption e) { + throw new TestAbortedException( + "Implementation does (gracefully) not provide coverage for any sopv interface version."); + } catch (RuntimeException e) { + throw new TestAbortedException("Implementation does not provide coverage for any sopv interface version."); + } + } } From ae2389cabf9dbab2972aa1aa40cc42042ee5cf92 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:45:29 +0100 Subject: [PATCH 005/110] Bump version to 10.0.0 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 434fdc5..80c8092 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '8.0.2' + shortVersion = '10.0.0' isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 From 7a825c7607e23530883e76abb5295bdc459d8389 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:41:44 +0100 Subject: [PATCH 006/110] Fix javadoc reference --- external-sop/src/main/kotlin/sop/external/ExternalSOP.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt index 3a0ef52..27c93ae 100644 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -16,7 +16,7 @@ import sop.external.operation.* import sop.operation.* /** - * Implementation of the {@link SOP} API using an external SOP binary. + * Implementation of the [SOP] API using an external SOP binary. * * Instantiate an [ExternalSOP] object for the given binary and the given [TempDirProvider] using * empty environment variables. From 7325cad6967d8ea13966ad788681d06d6cb86d9a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:42:54 +0100 Subject: [PATCH 007/110] Replace assumeTrue(false) with explicit TestAbortedException --- .../java/sop/testsuite/operation/VersionTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java index 73ba571..0b19d20 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.opentest4j.TestAbortedException; import sop.SOP; import java.util.stream.Stream; @@ -15,7 +16,6 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeTrue; @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") public class VersionTest extends AbstractSOPTest { @@ -59,7 +59,7 @@ public class VersionTest extends AbstractSOPTest { try { sop.version().getSopSpecVersion(); } catch (RuntimeException e) { - assumeTrue(false); // SOP backend does not support this operation yet + throw new TestAbortedException("SOP backend does not support 'version --sop-spec' yet."); } String sopSpec = sop.version().getSopSpecVersion(); From cd208c8942ebec73297663b597e27518d80eefb2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:43:39 +0100 Subject: [PATCH 008/110] Add test ckecking that BadData is thrown if KEYS is passed for CERTS --- .../sop/testsuite/operation/EncryptDecryptTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java index 51c117a..df824ca 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java @@ -327,4 +327,15 @@ public class EncryptDecryptTest extends AbstractSOPTest { .toByteArrayAndResult() .getBytes()); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void passingSecretKeysForPublicKeysFails(SOP sop) { + assertThrows(SOPGPException.BadData.class, () -> + sop.encrypt() + .withCert(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) + .toByteArrayAndResult() + .getBytes()); + } } From ed9b2f5fef7c9d8436c41d373e59555265abcfef Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:45:14 +0100 Subject: [PATCH 009/110] Move signature verification operations to sopv interface subset --- .../main/kotlin/sop/external/ExternalSOPV.kt | 53 ++++++++++ .../sop/external/operation/VersionExternal.kt | 4 + .../main/kotlin/sop/cli/picocli/SopVCLI.kt | 98 +++++++++++++++++++ .../sop/cli/picocli/commands/VersionCmd.kt | 6 ++ .../src/main/resources/msg_sop.properties | 2 + .../src/main/resources/msg_sop_de.properties | 2 + sop-java/src/main/kotlin/sop/SOP.kt | 26 +---- sop-java/src/main/kotlin/sop/SOPV.kt | 34 +++++++ .../src/main/kotlin/sop/operation/Version.kt | 10 ++ .../sop/testsuite/operation/VersionTest.java | 14 +++ 10 files changed, 224 insertions(+), 25 deletions(-) create mode 100644 external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt create mode 100644 sop-java/src/main/kotlin/sop/SOPV.kt diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt new file mode 100644 index 0000000..f22f947 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external + +import java.nio.file.Files +import java.util.* +import sop.SOPV +import sop.external.ExternalSOP.TempDirProvider +import sop.external.operation.DetachedVerifyExternal +import sop.external.operation.InlineVerifyExternal +import sop.external.operation.VersionExternal +import sop.operation.DetachedVerify +import sop.operation.InlineVerify +import sop.operation.Version + +/** + * Implementation of the [SOPV] API subset using an external sopv/sop binary. + * + * Instantiate an [ExternalSOPV] object for the given binary and the given [TempDirProvider] using + * empty environment variables. + * + * @param binaryName name / path of the sopv binary + * @param tempDirProvider custom tempDirProvider + */ +class ExternalSOPV( + private val binaryName: String, + private val properties: Properties = Properties(), + private val tempDirProvider: TempDirProvider = defaultTempDirProvider() +) : SOPV { + + override fun version(): Version = VersionExternal(binaryName, properties) + + override fun detachedVerify(): DetachedVerify = DetachedVerifyExternal(binaryName, properties) + + override fun inlineVerify(): InlineVerify = + InlineVerifyExternal(binaryName, properties, tempDirProvider) + + companion object { + + /** + * Default implementation of the [TempDirProvider] which stores temporary files in the + * systems temp dir ([Files.createTempDirectory]). + * + * @return default implementation + */ + @JvmStatic + fun defaultTempDirProvider(): TempDirProvider { + return TempDirProvider { Files.createTempDirectory("ext-sopv").toFile() } + } + } +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt index 7e13fc1..728f3b6 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt @@ -68,6 +68,10 @@ class VersionExternal(binary: String, environment: Properties) : Version { return null } + override fun getSopVVersion(): String { + return executeForLines(commandList.plus("--sopv")) + } + override fun getSopSpecVersion(): String { return executeForLines(commandList.plus("--sop-spec")) } diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt new file mode 100644 index 0000000..9a8b4b4 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli + +import java.util.* +import kotlin.system.exitProcess +import picocli.AutoComplete +import picocli.CommandLine +import sop.SOPV +import sop.cli.picocli.commands.* +import sop.exception.SOPGPException + +@CommandLine.Command( + name = "sopv", + resourceBundle = "msg_sop", + exitCodeOnInvalidInput = SOPGPException.UnsupportedSubcommand.EXIT_CODE, + subcommands = + [ + // Meta subcommands + VersionCmd::class, + // signature verification subcommands + VerifyCmd::class, + InlineVerifyCmd::class, + // misc + CommandLine.HelpCommand::class, + AutoComplete.GenerateCompletion::class]) +class SopVCLI { + + companion object { + @JvmStatic private var sopvInstance: SOPV? = null + + @JvmStatic + fun getSopV(): SOPV = + checkNotNull(sopvInstance) { cliMsg.getString("sop.error.runtime.no_backend_set") } + + @JvmStatic + fun setSopVInstance(sopv: SOPV?) { + sopvInstance = sopv + } + + @JvmField var cliMsg: ResourceBundle = ResourceBundle.getBundle("msg_sop") + + @JvmField var EXECUTABLE_NAME = "sopv" + + @JvmField + @CommandLine.Option(names = ["--stacktrace"], scope = CommandLine.ScopeType.INHERIT) + var stacktrace = false + + @JvmStatic + fun main(vararg args: String) { + val exitCode = execute(*args) + if (exitCode != 0) { + exitProcess(exitCode) + } + } + + @JvmStatic + fun execute(vararg args: String): Int { + // Set locale + CommandLine(InitLocale()).parseArgs(*args) + + // Re-set bundle with updated locale + cliMsg = ResourceBundle.getBundle("msg_sop") + + return CommandLine(SopVCLI::class.java) + .apply { + // explicitly set help command resource bundle + subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help")) + // Hide generate-completion command + subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true) + // overwrite executable name + commandName = EXECUTABLE_NAME + // setup exception handling + executionExceptionHandler = SOPExecutionExceptionHandler() + exitCodeExceptionMapper = SOPExceptionExitCodeMapper() + isCaseInsensitiveEnumValuesAllowed = true + } + .execute(*args) + } + } + + /** + * Control the locale. + * + * @see Picocli Readme + */ + @CommandLine.Command + class InitLocale { + @CommandLine.Option(names = ["-l", "--locale"], descriptionKey = "sop.locale") + fun setLocale(locale: String) = Locale.setDefault(Locale(locale)) + + @CommandLine.Unmatched + var remainder: MutableList = + mutableListOf() // ignore any other parameters and options in the first parsing phase + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt index 75197fe..8b1936a 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt @@ -22,6 +22,7 @@ class VersionCmd : AbstractSopCmd() { @Option(names = ["--extended"]) var extended: Boolean = false @Option(names = ["--backend"]) var backend: Boolean = false @Option(names = ["--sop-spec"]) var sopSpec: Boolean = false + @Option(names = ["--sopv"]) var sopv: Boolean = false } override fun run() { @@ -47,5 +48,10 @@ class VersionCmd : AbstractSopCmd() { println(version.getSopSpecVersion()) return } + + if (exclusive!!.sopv) { + println(version.getSopVVersion()) + return + } } } diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 52c5368..7979eb3 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -2,7 +2,9 @@ # # SPDX-License-Identifier: Apache-2.0 sop.name=sop +sopv.name=sopv usage.header=Stateless OpenPGP Protocol +sopv.usage.header=Stateless OpenPGP Protocol - Signature Verification Interface Subset locale=Locale for description texts # Generic diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 5900f39..40a316d 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -2,7 +2,9 @@ # # SPDX-License-Identifier: Apache-2.0 sop.name=sop +sopv.name=sopv usage.header=Stateless OpenPGP Protocol +sopv.usage.header=Stateless OpenPGP Protocol - Signature Verification Interface Subset locale=Gebietsschema für Beschreibungstexte # Generic diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index e01763a..7fdd414 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -9,16 +9,13 @@ import sop.operation.ChangeKeyPassword import sop.operation.Dearmor import sop.operation.Decrypt import sop.operation.DetachedSign -import sop.operation.DetachedVerify 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.ListProfiles import sop.operation.RevokeKey -import sop.operation.Version /** * Stateless OpenPGP Interface. This class provides a stateless interface to various OpenPGP related @@ -26,10 +23,7 @@ import sop.operation.Version * intended for reuse. If you for example need to generate multiple keys, make a dedicated call to * [generateKey] once per key generation. */ -interface SOP { - - /** Get information about the implementations name and version. */ - fun version(): Version +interface SOP : SOPV { /** Generate a secret key. */ fun generateKey(): GenerateKey @@ -53,24 +47,6 @@ interface SOP { */ fun inlineSign(): InlineSign - /** - * Verify detached signatures. If you need to verify an inline-signed message, use - * [inlineVerify] instead. - */ - fun verify(): DetachedVerify = detachedVerify() - - /** - * Verify detached signatures. If you need to verify an inline-signed message, use - * [inlineVerify] instead. - */ - fun detachedVerify(): DetachedVerify - - /** - * Verify signatures of an inline-signed message. If you need to verify detached signatures over - * a message, use [detachedVerify] instead. - */ - fun inlineVerify(): InlineVerify - /** Detach signatures from an inline signed message. */ fun inlineDetach(): InlineDetach diff --git a/sop-java/src/main/kotlin/sop/SOPV.kt b/sop-java/src/main/kotlin/sop/SOPV.kt new file mode 100644 index 0000000..d331559 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/SOPV.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import sop.operation.DetachedVerify +import sop.operation.InlineVerify +import sop.operation.Version + +/** Subset of [SOP] implementing only OpenPGP signature verification. */ +interface SOPV { + + /** Get information about the implementations name and version. */ + fun version(): Version + + /** + * Verify detached signatures. If you need to verify an inline-signed message, use + * [inlineVerify] instead. + */ + fun verify(): DetachedVerify = detachedVerify() + + /** + * Verify detached signatures. If you need to verify an inline-signed message, use + * [inlineVerify] instead. + */ + fun detachedVerify(): DetachedVerify + + /** + * Verify signatures of an inline-signed message. If you need to verify detached signatures over + * a message, use [detachedVerify] instead. + */ + fun inlineVerify(): InlineVerify +} diff --git a/sop-java/src/main/kotlin/sop/operation/Version.kt b/sop-java/src/main/kotlin/sop/operation/Version.kt index 9b3bd8a..5f26491 100644 --- a/sop-java/src/main/kotlin/sop/operation/Version.kt +++ b/sop-java/src/main/kotlin/sop/operation/Version.kt @@ -4,6 +4,9 @@ package sop.operation +import kotlin.jvm.Throws +import sop.exception.SOPGPException + interface Version { /** @@ -97,4 +100,11 @@ interface Version { * @return remarks or null */ fun getSopSpecImplementationRemarks(): String? + + /** + * Return the single-line SEMVER version of the sopv interface subset it provides complete + * coverage of. If the implementation does not provide complete coverage for any sopv interface, + * this method throws an [SOPGPException.UnsupportedOption] instead. + */ + @Throws(SOPGPException.UnsupportedOption::class) fun getSopVVersion(): String } diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java index 0b19d20..f836935 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.opentest4j.TestAbortedException; import sop.SOP; +import sop.exception.SOPGPException; import java.util.stream.Stream; @@ -72,4 +73,17 @@ public class VersionTest extends AbstractSOPTest { int sopRevision = sop.version().getSopSpecRevisionNumber(); assertTrue(sop.version().getSopSpecRevisionName().endsWith("" + sopRevision)); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void sopVVersionTest(SOP sop) { + try { + sop.version().getSopVVersion(); + } catch (SOPGPException.UnsupportedOption e) { + throw new TestAbortedException( + "Implementation does (gracefully) not provide coverage for any sopv interface version."); + } catch (RuntimeException e) { + throw new TestAbortedException("Implementation does not provide coverage for any sopv interface version."); + } + } } From 3643aff082597ef31392341ffbd17eda54c8cbe3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:45:29 +0100 Subject: [PATCH 010/110] Bump version to 10.0.0 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 434fdc5..80c8092 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '8.0.2' + shortVersion = '10.0.0' isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 From bdbc9593c8c79cad064d7a78be5d71ef04984ce7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 21 Mar 2024 13:43:12 +0100 Subject: [PATCH 011/110] Update spec revision and badge link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa7e5a5..0efd41f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ SPDX-License-Identifier: Apache-2.0 # SOP for Java [![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java) -[![Spec Revision: 8](https://img.shields.io/badge/Spec%20Revision-8-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/08/) +[![Spec Revision: 10](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/10/) [![Coverage Status](https://coveralls.io/repos/github/pgpainless/sop-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/sop-java?branch=main) [![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/sop-java)](https://api.reuse.software/info/github.com/pgpainless/sop-java) From bfa97aede80947e5f94ea22a8e8b6fc0b7a4e067 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 21 Mar 2024 13:43:25 +0100 Subject: [PATCH 012/110] Add description of external-sop module --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0efd41f..baeb874 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ The repository contains the following modules: * [sop-java](/sop-java) defines a set of Java interfaces describing the Stateless OpenPGP Protocol. * [sop-java-picocli](/sop-java-picocli) contains a wrapper application that transforms the `sop-java` API into a command line application compatible with the SOP-CLI specification. +* [external-sop](/external-sop) contains an API implementation that can be used to forward API calls to a SOP executable, +allowing to delegate the implementation logic to an arbitrary SOP CLI implementation. ## Known Implementations (Please expand!) From 30f7ca90cdc3bf36eeae11e9d06dab5a53658e04 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 21 Mar 2024 13:49:32 +0100 Subject: [PATCH 013/110] Bump logback-core and logback-classic from 1.2.11 to 1.2.13 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 80c8092..cae50b2 100644 --- a/version.gradle +++ b/version.gradle @@ -12,7 +12,7 @@ allprojects { jsrVersion = '3.0.2' junitVersion = '5.8.2' junitSysExitVersion = '1.1.2' - logbackVersion = '1.2.11' + logbackVersion = '1.2.13' mockitoVersion = '4.5.1' picocliVersion = '4.6.3' slf4jVersion = '1.7.36' From 4b2875d5727e3283f481fa433b929b766434d994 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 21 Mar 2024 13:53:26 +0100 Subject: [PATCH 014/110] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f188b0b..819523e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 10.0.0 +- Update implementation to [SOP Specification revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html). + - Throw `BadData` when passing KEYS where CERTS are expected + - Introduce `sopv` interface subset with revision `1.0` + - Add `sop version --sopv` + ## 8.0.1 - `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty. From da6cba1d5576f30211bbe45f6623f07b45385b63 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 21 Mar 2024 14:01:48 +0100 Subject: [PATCH 015/110] SOP-Java 10.0.0 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index cae50b2..e686a30 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '10.0.0' - isSnapshot = true + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From cbbdd09472af5c9d163cf57481608814f92d0db5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 21 Mar 2024 14:06:42 +0100 Subject: [PATCH 016/110] SOP-Java 10.0.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index e686a30..94d4c8d 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '10.0.0' - isSnapshot = false + shortVersion = '10.0.1' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From e39cc7f0ac24e3f2c99bda7c33b53015ea16e283 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 27 Mar 2024 21:50:01 +0100 Subject: [PATCH 017/110] Remove deprecated junit5-system-exit Replaced with custom test DSL that avoids System.exit --- sop-java-picocli/build.gradle | 5 +- .../test/java/sop/cli/picocli/SOPTest.java | 11 +- .../cli/picocli/commands/ArmorCmdTest.java | 12 +- .../cli/picocli/commands/DearmorCmdTest.java | 8 +- .../cli/picocli/commands/DecryptCmdTest.java | 137 +++++----- .../cli/picocli/commands/EncryptCmdTest.java | 121 +++++---- .../picocli/commands/ExtractCertCmdTest.java | 22 +- .../picocli/commands/GenerateKeyCmdTest.java | 30 ++- .../picocli/commands/InlineDetachCmdTest.java | 12 +- .../sop/cli/picocli/commands/SignCmdTest.java | 61 +++-- .../cli/picocli/commands/VerifyCmdTest.java | 70 ++++-- .../cli/picocli/commands/VersionCmdTest.java | 41 ++- .../assertions/SopExecutionAssertions.java | 235 ++++++++++++++++++ 13 files changed, 550 insertions(+), 215 deletions(-) create mode 100644 sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java diff --git a/sop-java-picocli/build.gradle b/sop-java-picocli/build.gradle index 438ef50..0596ad3 100644 --- a/sop-java-picocli/build.gradle +++ b/sop-java-picocli/build.gradle @@ -12,15 +12,12 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - // Testing Exit Codes in JUnit - // https://todd.ginsberg.com/post/testing-system-exit/ - testImplementation "com.ginsberg:junit5-system-exit:$junitSysExitVersion" - // Mocking Components testImplementation "org.mockito:mockito-core:$mockitoVersion" // SOP implementation(project(":sop-java")) + testImplementation(testFixtures(project(":sop-java"))) // CLI implementation "info.picocli:picocli:$picocliVersion" 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 68b32be..fe49472 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 @@ -6,12 +6,13 @@ package sop.cli.picocli; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; +import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedSubcommand; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.Test; import sop.SOP; import sop.exception.SOPGPException; @@ -34,20 +35,18 @@ import sop.operation.Version; public class SOPTest { @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedSubcommand.EXIT_CODE) public void assertExitOnInvalidSubcommand() { SOP sop = mock(SOP.class); SopCLI.setSopInstance(sop); - SopCLI.main(new String[] {"invalid"}); + assertUnsupportedSubcommand(() -> SopCLI.execute("invalid")); } @Test - @ExpectSystemExitWithStatus(1) public void assertThrowsIfNoSOPBackendSet() { SopCLI.setSopInstance(null); - // At this point, no SOP backend is set, so an InvalidStateException triggers exit(1) - SopCLI.main(new String[] {"armor"}); + // At this point, no SOP backend is set, so an InvalidStateException triggers error code 1 + assertGenericError(() -> SopCLI.execute("armor")); } @Test diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java index da211e0..3dd4c7c 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java @@ -4,8 +4,6 @@ package sop.cli.picocli.commands; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import com.ginsberg.junit.exit.FailOnSystemExit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.Ready; @@ -24,6 +22,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; public class ArmorCmdTest { @@ -42,24 +42,22 @@ public class ArmorCmdTest { @Test public void assertDataIsAlwaysCalled() throws SOPGPException.BadData, IOException { - SopCLI.main(new String[] {"armor"}); + assertSuccess(() -> SopCLI.execute("armor")); verify(armor, times(1)).data((InputStream) any()); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void ifBadDataExit41() throws SOPGPException.BadData, IOException { when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"armor"}); + assertBadData(() -> SopCLI.execute("armor")); } @Test - @FailOnSystemExit public void ifNoErrorsNoExit() { when(sop.armor()).thenReturn(armor); - SopCLI.main(new String[] {"armor"}); + assertSuccess(() -> SopCLI.execute("armor")); } private static Ready nopReady() { diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java index 875eaed..b0a9fd8 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java @@ -9,12 +9,13 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.Ready; @@ -48,14 +49,13 @@ public class DearmorCmdTest { @Test public void assertDataIsCalled() throws IOException, SOPGPException.BadData { - SopCLI.main(new String[] {"dearmor"}); + assertSuccess(() -> SopCLI.execute("dearmor")); verify(dearmor, times(1)).data((InputStream) any()); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void assertBadDataCausesExit41() throws IOException, SOPGPException.BadData { when(dearmor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException("invalid armor"))); - SopCLI.main(new String[] {"dearmor"}); + assertBadData(() -> SopCLI.execute("dearmor")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java index 62070c2..b7cb8bc 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java @@ -4,7 +4,6 @@ package sop.cli.picocli.commands; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatcher; @@ -42,6 +41,18 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertCannotDecrypt; +import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; +import static sop.testsuite.assertions.SopExecutionAssertions.assertIncompleteVerification; +import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyIsProtected; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput; +import static sop.testsuite.assertions.SopExecutionAssertions.assertOutputExists; +import static sop.testsuite.assertions.SopExecutionAssertions.assertPasswordNotHumanReadable; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedAsymmetricAlgo; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption; public class DecryptCmdTest { @@ -74,47 +85,47 @@ public class DecryptCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void missingArgumentsExceptionCausesExit19() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException { when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.MissingArg("Missing arguments.")); - SopCLI.main(new String[] {"decrypt"}); + assertMissingArg(() -> SopCLI.execute("decrypt")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void badDataExceptionCausesExit41() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException { when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"decrypt"}); + assertBadData(() -> SopCLI.execute("decrypt")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.PasswordNotHumanReadable.EXIT_CODE) public void assertNotHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable"); when(decrypt.withPassword(any())).thenThrow(new SOPGPException.PasswordNotHumanReadable()); - SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertPasswordNotHumanReadable(() -> + SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath()) + ); } @Test public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("orange"); - SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertSuccess(() -> SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath())); verify(decrypt, times(1)).withPassword("orange"); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("swordfish"); when(decrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Decrypting with password not supported.")); - SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath()) + ); } @Test public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption { Date now = new Date(); - SopCLI.main(new String[] {"decrypt"}); + assertSuccess(() -> SopCLI.execute("decrypt")); verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME); verify(decrypt, times(1)).verifyNotAfter( ArgumentMatchers.argThat(argument -> { @@ -125,7 +136,8 @@ public class DecryptCmdTest { @Test public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"decrypt", "--verify-not-before", "-", "--verify-not-after", "-"}); + assertSuccess(() -> + SopCLI.execute("decrypt", "--verify-not-before", "-", "--verify-not-after", "-")); verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME); verify(decrypt, times(1)).verifyNotAfter(AbstractSopCmd.END_OF_TIME); } @@ -138,54 +150,57 @@ public class DecryptCmdTest { return Math.abs(now.getTime() - argument.getTime()) <= 1000; }; - SopCLI.main(new String[] {"decrypt", "--verify-not-before", "now", "--verify-not-after", "now"}); + assertSuccess(() -> + SopCLI.execute("decrypt", "--verify-not-before", "now", "--verify-not-after", "now")); verify(decrypt, times(1)).verifyNotAfter(ArgumentMatchers.argThat(isMaxOneSecOff)); verify(decrypt, times(1)).verifyNotBefore(ArgumentMatchers.argThat(isMaxOneSecOff)); } @Test - @ExpectSystemExitWithStatus(1) public void assertMalformedDateInNotBeforeCausesExit1() { // ParserException causes exit(1) - SopCLI.main(new String[] {"decrypt", "--verify-not-before", "invalid"}); + assertGenericError(() -> + SopCLI.execute("decrypt", "--verify-not-before", "invalid")); } @Test - @ExpectSystemExitWithStatus(1) public void assertMalformedDateInNotAfterCausesExit1() { // ParserException causes exit(1) - SopCLI.main(new String[] {"decrypt", "--verify-not-after", "invalid"}); + assertGenericError(() -> + SopCLI.execute("decrypt", "--verify-not-after", "invalid")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertUnsupportedNotAfterCausesExit37() throws SOPGPException.UnsupportedOption { - when(decrypt.verifyNotAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); - SopCLI.main(new String[] {"decrypt", "--verify-not-after", "now"}); + when(decrypt.verifyNotAfter(any())).thenThrow( + new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); + assertUnsupportedOption(() -> + SopCLI.execute("decrypt", "--verify-not-after", "now")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertUnsupportedNotBeforeCausesExit37() throws SOPGPException.UnsupportedOption { - when(decrypt.verifyNotBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); - SopCLI.main(new String[] {"decrypt", "--verify-not-before", "now"}); + when(decrypt.verifyNotBefore(any())).thenThrow( + new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); + assertUnsupportedOption(() -> + SopCLI.execute("decrypt", "--verify-not-before", "now")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE) public void assertExistingSessionKeyOutFileCausesExit59() throws IOException { File tempFile = File.createTempFile("existing-session-key-", ".tmp"); tempFile.deleteOnExit(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); + assertOutputExists(() -> + SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertWhenSessionKeyCannotBeExtractedExit37() throws IOException { Path tempDir = Files.createTempDirectory("session-key-out-dir"); File tempFile = new File(tempDir.toFile(), "session-key"); tempFile.deleteOnExit(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath())); } @Test @@ -210,8 +225,10 @@ public class DecryptCmdTest { File verificationsFile = new File(tempDir.toFile(), "verifications"); File keyFile = new File(tempDir.toFile(), "key.asc"); keyFile.createNewFile(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(), - "--verifications-out", verificationsFile.getAbsolutePath(), "--verify-with", keyFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(), + "--verifications-out", verificationsFile.getAbsolutePath(), "--verify-with", + keyFile.getAbsolutePath())); ByteArrayOutputStream bytesInFile = new ByteArrayOutputStream(); try (FileInputStream fileIn = new FileInputStream(sessionKeyFile)) { @@ -241,10 +258,10 @@ public class DecryptCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.CannotDecrypt.EXIT_CODE) public void assertUnableToDecryptExceptionResultsInExit29() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.CannotDecrypt()); - SopCLI.main(new String[] {"decrypt"}); + assertCannotDecrypt(() -> + SopCLI.execute("decrypt")); } @Test @@ -258,30 +275,32 @@ public class DecryptCmdTest { return new DecryptionResult(null, Collections.emptyList()); } }); - SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath(), "--verifications-out", verifyOut.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("decrypt", "--verify-with", tempFile.getAbsolutePath(), "--verifications-out", + verifyOut.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void badDataInVerifyWithCausesExit41() throws IOException, SOPGPException.BadData { when(decrypt.verifyWithCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File tempFile = File.createTempFile("verify-with-", ".tmp"); - SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("decrypt", "--verify-with", tempFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void unexistentCertFileCausesExit61() { - SopCLI.main(new String[] {"decrypt", "--verify-with", "invalid"}); + assertMissingInput(() -> + SopCLI.execute("decrypt", "--verify-with", "invalid")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE) public void existingVerifyOutCausesExit59() throws IOException { File certFile = File.createTempFile("existing-verify-out-cert", ".asc"); File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp"); - SopCLI.main(new String[] {"decrypt", "--verifications-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); + assertOutputExists(() -> SopCLI.execute("decrypt", "--verifications-out", + existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath())); } @Test @@ -305,7 +324,9 @@ public class DecryptCmdTest { } }); - SopCLI.main(new String[] {"decrypt", "--verifications-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("decrypt", "--verifications-out", verifyOut.getAbsolutePath(), + "--verify-with", certFile.getAbsolutePath())); try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) { String line = reader.readLine(); assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line); @@ -320,66 +341,64 @@ public class DecryptCmdTest { File sessionKeyFile1 = TestFileUtil.writeTempStringFile(key1.toString()); File sessionKeyFile2 = TestFileUtil.writeTempStringFile(key2.toString()); - SopCLI.main(new String[] {"decrypt", - "--with-session-key", sessionKeyFile1.getAbsolutePath(), - "--with-session-key", sessionKeyFile2.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("decrypt", + "--with-session-key", sessionKeyFile1.getAbsolutePath(), + "--with-session-key", sessionKeyFile2.getAbsolutePath())); verify(decrypt).withSessionKey(key1); verify(decrypt).withSessionKey(key2); } @Test - @ExpectSystemExitWithStatus(1) public void assertMalformedSessionKeysResultInExit1() throws IOException { File sessionKeyFile = TestFileUtil.writeTempStringFile("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137"); - SopCLI.main(new String[] {"decrypt", - "--with-session-key", sessionKeyFile.getAbsolutePath()}); + assertGenericError(() -> + SopCLI.execute("decrypt", + "--with-session-key", sessionKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void assertBadDataInKeysResultsInExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); + assertBadData(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void assertKeyFileNotFoundCausesExit61() { - SopCLI.main(new String[] {"decrypt", "nonexistent-key"}); + assertMissingInput(() -> SopCLI.execute("decrypt", "nonexistent-key")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE) public void assertProtectedKeyCausesExit67() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); + assertKeyIsProtected(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) public void assertUnsupportedAlgorithmExceptionCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new IOException())); File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); + assertUnsupportedAsymmetricAlgo(() -> + SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void assertMissingPassphraseFileCausesExit61() { - SopCLI.main(new String[] {"decrypt", "--with-password", "missing"}); + assertMissingInput(() -> + SopCLI.execute("decrypt", "--with-password", "missing")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void assertMissingSessionKeyFileCausesExit61() { - SopCLI.main(new String[] {"decrypt", "--with-session-key", "missing"}); + assertMissingInput(() -> + SopCLI.execute("decrypt", "--with-session-key", "missing")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.IncompleteVerification.EXIT_CODE) public void verifyOutWithoutVerifyWithCausesExit23() { - SopCLI.main(new String[] {"decrypt", "--verifications-out", "out.file"}); + assertIncompleteVerification(() -> + SopCLI.execute("decrypt", "--verifications-out", "out.file")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java index 09346af..85ae052 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java @@ -4,7 +4,6 @@ package sop.cli.picocli.commands; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,6 +27,17 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertCertCannotEncrypt; +import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; +import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyCannotSign; +import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyIsProtected; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput; +import static sop.testsuite.assertions.SopExecutionAssertions.assertPasswordNotHumanReadable; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedAsymmetricAlgo; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption; public class EncryptCmdTest { @@ -50,48 +60,50 @@ public class EncryptCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) - public void missingBothPasswordAndCertFileCauseExit19() { - SopCLI.main(new String[] {"encrypt", "--no-armor"}); + public void missingBothPasswordAndCertFileCausesMissingArg() { + assertMissingArg(() -> + SopCLI.execute("encrypt", "--no-armor")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void as_unsupportedEncryptAsCausesExit37() throws SOPGPException.UnsupportedOption { + public void as_unsupportedEncryptAsCausesUnsupportedOption() throws SOPGPException.UnsupportedOption { when(encrypt.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting encryption mode not supported.")); - SopCLI.main(new String[] {"encrypt", "--as", "Binary"}); + assertUnsupportedOption(() -> + SopCLI.execute("encrypt", "--as", "Binary")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void as_invalidModeOptionCausesExit37() { - SopCLI.main(new String[] {"encrypt", "--as", "invalid"}); + public void as_invalidModeOptionCausesUnsupportedOption() { + assertUnsupportedOption(() -> + SopCLI.execute("encrypt", "--as", "invalid")); } @Test public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("0rbit"); for (EncryptAs mode : EncryptAs.values()) { - SopCLI.main(new String[] {"encrypt", "--as", mode.name(), "--with-password", passwordFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("encrypt", "--as", mode.name(), + "--with-password", passwordFile.getAbsolutePath())); verify(encrypt, times(1)).mode(mode); } } @Test - @ExpectSystemExitWithStatus(SOPGPException.PasswordNotHumanReadable.EXIT_CODE) - public void withPassword_notHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { + public void withPassword_notHumanReadablePasswordCausesPWNotHumanReadable() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable()); File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertPasswordNotHumanReadable(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void withPassword_unsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { + public void withPassword_unsupportedWithPasswordCausesUnsupportedOption() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported.")); File passwordFile = TestFileUtil.writeTempStringFile("orange"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); } @Test @@ -99,99 +111,107 @@ public class EncryptCmdTest { File keyFile1 = File.createTempFile("sign-with-1-", ".asc"); File keyFile2 = File.createTempFile("sign-with-2-", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("password"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile1.getAbsolutePath(), "--sign-with", keyFile2.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), + "--sign-with", keyFile1.getAbsolutePath(), + "--sign-with", keyFile2.getAbsolutePath())); verify(encrypt, times(2)).signWith((InputStream) any()); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) - public void signWith_nonExistentKeyFileCausesExit61() { - SopCLI.main(new String[] {"encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"}); + public void signWith_nonExistentKeyFileCausesMissingInput() { + assertMissingInput(() -> + SopCLI.execute("encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE) - public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + public void signWith_keyIsProtectedCausesKeyIsProtected() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); File keyFile = File.createTempFile("sign-with", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("starship"); - SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", passwordFile.getAbsolutePath()}); + assertKeyIsProtected(() -> + SopCLI.execute("encrypt", "--sign-with", keyFile.getAbsolutePath(), + "--with-password", passwordFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) - public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + public void signWith_unsupportedAsymmetricAlgoCausesUnsupportedAsymAlgo() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); File keyFile = File.createTempFile("sign-with", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("123456"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); + assertUnsupportedAsymmetricAlgo(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), + "--sign-with", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyCannotSign.EXIT_CODE) - public void signWith_certCannotSignCausesExit79() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { + public void signWith_certCannotSignCausesKeyCannotSign() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign()); File keyFile = File.createTempFile("sign-with", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("dragon"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); + assertKeyCannotSign(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), + "--sign-with", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) - public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + public void signWith_badDataCausesBadData() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File keyFile = File.createTempFile("sign-with", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("orange"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), + "--sign-with", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) - public void cert_nonExistentCertFileCausesExit61() { - SopCLI.main(new String[] {"encrypt", "invalid.asc"}); + public void cert_nonExistentCertFileCausesMissingInput() { + assertMissingInput(() -> + SopCLI.execute("encrypt", "invalid.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) - public void cert_unsupportedAsymmetricAlgorithmCausesExit13() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { + public void cert_unsupportedAsymmetricAlgorithmCausesUnsupportedAsymAlg() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); + assertUnsupportedAsymmetricAlgo(() -> + SopCLI.execute("encrypt", certFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.CertCannotEncrypt.EXIT_CODE) - public void cert_certCannotEncryptCausesExit17() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { + public void cert_certCannotEncryptCausesCertCannotEncrypt() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.CertCannotEncrypt("Certificate cannot encrypt.", new Exception())); File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); + assertCertCannotEncrypt(() -> + SopCLI.execute("encrypt", certFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) - public void cert_badDataCausesExit41() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { + public void cert_badDataCausesBadData() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("encrypt", certFile.getAbsolutePath())); } @Test public void noArmor_notCalledByDefault() throws IOException { File passwordFile = TestFileUtil.writeTempStringFile("clownfish"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); verify(encrypt, never()).noArmor(); } @Test public void noArmor_callGetsPassedDown() throws IOException { File passwordFile = TestFileUtil.writeTempStringFile("monkey"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--no-armor"}); + assertSuccess(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), "--no-armor")); verify(encrypt, times(1)).noArmor(); } @Test - @ExpectSystemExitWithStatus(1) - public void writeTo_ioExceptionCausesExit1() throws IOException { + public void writeTo_ioExceptionCausesGenericError() throws IOException { when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult() { @Override public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException { @@ -199,6 +219,7 @@ public class EncryptCmdTest { } }); File passwordFile = TestFileUtil.writeTempStringFile("wildcat"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertGenericError(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java index 12f837d..3b046a0 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java @@ -10,12 +10,14 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.Ready; @@ -45,32 +47,34 @@ public class ExtractCertCmdTest { @Test public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"extract-cert"}); + assertSuccess(() -> + SopCLI.execute("extract-cert")); verify(extractCert, never()).noArmor(); } @Test public void noArmor_passedDown() { - SopCLI.main(new String[] {"extract-cert", "--no-armor"}); + assertSuccess(() -> + SopCLI.execute("extract-cert", "--no-armor")); verify(extractCert, times(1)).noArmor(); } @Test - @ExpectSystemExitWithStatus(1) - public void key_ioExceptionCausesExit1() throws IOException, SOPGPException.BadData { + public void key_ioExceptionCausesGenericError() throws IOException, SOPGPException.BadData { when(extractCert.key((InputStream) any())).thenReturn(new Ready() { @Override public void writeTo(OutputStream outputStream) throws IOException { throw new IOException(); } }); - SopCLI.main(new String[] {"extract-cert"}); + assertGenericError(() -> + SopCLI.execute("extract-cert")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) - public void key_badDataCausesExit41() throws IOException, SOPGPException.BadData { + public void key_badDataCausesBadData() throws IOException, SOPGPException.BadData { when(extractCert.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"extract-cert"}); + assertBadData(() -> + SopCLI.execute("extract-cert")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java index e7ebf1a..126c851 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java @@ -10,11 +10,14 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedAsymmetricAlgo; import java.io.IOException; import java.io.OutputStream; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InOrder; @@ -47,19 +50,22 @@ public class GenerateKeyCmdTest { @Test public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"generate-key", "Alice"}); + assertSuccess(() -> + SopCLI.execute("generate-key", "Alice")); verify(generateKey, never()).noArmor(); } @Test public void noArmor_passedDown() { - SopCLI.main(new String[] {"generate-key", "--no-armor", "Alice"}); + assertSuccess(() -> + SopCLI.execute("generate-key", "--no-armor", "Alice")); verify(generateKey, times(1)).noArmor(); } @Test public void userId_multipleUserIdsPassedDownInProperOrder() { - SopCLI.main(new String[] {"generate-key", "Alice ", "Bob "}); + assertSuccess(() -> + SopCLI.execute("generate-key", "Alice ", "Bob ")); InOrder inOrder = Mockito.inOrder(generateKey); inOrder.verify(generateKey).userId("Alice "); @@ -69,30 +75,32 @@ public class GenerateKeyCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void missingArgumentCausesExit19() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { // TODO: RFC4880-bis and the current Stateless OpenPGP CLI spec allow keys to have no user-ids, // so we might want to change this test in the future. when(generateKey.generate()).thenThrow(new SOPGPException.MissingArg("Missing user-id.")); - SopCLI.main(new String[] {"generate-key"}); + assertMissingArg(() -> + SopCLI.execute("generate-key")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) public void unsupportedAsymmetricAlgorithmCausesExit13() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { when(generateKey.generate()).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); - SopCLI.main(new String[] {"generate-key", "Alice"}); + assertUnsupportedAsymmetricAlgo(() -> + SopCLI.execute("generate-key", "Alice")); + } @Test - @ExpectSystemExitWithStatus(1) - public void ioExceptionCausesExit1() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { + public void ioExceptionCausesGenericError() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { when(generateKey.generate()).thenReturn(new Ready() { @Override public void writeTo(OutputStream outputStream) throws IOException { throw new IOException(); } }); - SopCLI.main(new String[] {"generate-key", "Alice"}); + + assertGenericError(() -> + SopCLI.execute("generate-key", "Alice")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java index 3a16c61..a230aaa 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java @@ -4,7 +4,6 @@ package sop.cli.picocli.commands; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.ReadyWithResult; @@ -26,6 +25,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; public class InlineDetachCmdTest { @@ -41,9 +42,9 @@ public class InlineDetachCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) - public void testMissingSignaturesOutResultsInExit19() { - SopCLI.main(new String[] {"inline-detach"}); + public void testMissingSignaturesOutResultsInMissingArg() { + assertMissingArg(() -> + SopCLI.execute("inline-detach")); } @Test @@ -67,7 +68,8 @@ public class InlineDetachCmdTest { } }); - SopCLI.main(new String[] {"inline-detach", "--signatures-out", tempFile.getAbsolutePath(), "--no-armor"}); + assertSuccess(() -> + SopCLI.execute("inline-detach", "--signatures-out", tempFile.getAbsolutePath(), "--no-armor")); verify(inlineDetach, times(1)).noArmor(); verify(inlineDetach, times(1)).message((InputStream) any()); } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java index c3d6b59..324d39a 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java @@ -10,13 +10,20 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertExpectedText; +import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; +import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyIsProtected; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.ReadyWithResult; @@ -54,70 +61,77 @@ public class SignCmdTest { @Test public void as_optionsAreCaseInsensitive() { - SopCLI.main(new String[] {"sign", "--as", "Binary", keyFile.getAbsolutePath()}); - SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); - SopCLI.main(new String[] {"sign", "--as", "BINARY", keyFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("sign", "--as", "Binary", keyFile.getAbsolutePath())); + assertSuccess(() -> + SopCLI.execute("sign", "--as", "binary", keyFile.getAbsolutePath())); + assertSuccess(() -> + SopCLI.execute("sign", "--as", "BINARY", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void as_invalidOptionCausesExit37() { - SopCLI.main(new String[] {"sign", "--as", "Invalid", keyFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("sign", "--as", "Invalid", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { when(detachedSign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting signing mode not supported.")); - SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("sign", "--as", "binary", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void key_nonExistentKeyFileCausesExit61() { - SopCLI.main(new String[] {"sign", "invalid.asc"}); + assertMissingInput(() -> + SopCLI.execute("sign", "invalid.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE) public void key_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertKeyIsProtected(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void key_missingKeyFileCausesExit19() { - SopCLI.main(new String[] {"sign"}); + assertMissingArg(() -> + SopCLI.execute("sign")); } @Test public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); verify(detachedSign, never()).noArmor(); } @Test public void noArmor_passedDown() { - SopCLI.main(new String[] {"sign", "--no-armor", keyFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("sign", "--no-armor", keyFile.getAbsolutePath())); verify(detachedSign, times(1)).noArmor(); } @Test public void withKeyPassword_passedDown() { - SopCLI.main(new String[] {"sign", "--with-key-password", passFile.getAbsolutePath(), keyFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("sign", + "--with-key-password", passFile.getAbsolutePath(), + keyFile.getAbsolutePath())); verify(detachedSign, times(1)).withKeyPassword("sw0rdf1sh"); } @Test - @ExpectSystemExitWithStatus(1) public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText { when(detachedSign.data((InputStream) any())).thenReturn(new ReadyWithResult() { @Override @@ -125,13 +139,14 @@ public class SignCmdTest { throw new IOException(); } }); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertGenericError(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.ExpectedText.EXIT_CODE) public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText { when(detachedSign.data((InputStream) any())).thenThrow(new SOPGPException.ExpectedText()); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertExpectedText(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java index 50a8043..3c9724f 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java @@ -10,6 +10,11 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput; +import static sop.testsuite.assertions.SopExecutionAssertions.assertNoSignature; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption; import java.io.ByteArrayOutputStream; import java.io.File; @@ -21,7 +26,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -76,60 +80,75 @@ public class VerifyCmdTest { @Test public void notAfter_passedDown() throws SOPGPException.UnsupportedOption, ParseException { 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()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-after", "2019-10-29T18:36:45Z", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notAfter(date); } @Test public void notAfter_now() throws SOPGPException.UnsupportedOption { Date now = new Date(); - SopCLI.main(new String[] {"verify", "--not-after", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-after", "now", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notAfter(dateMatcher(now)); } @Test public void notAfter_dashCountsAsEndOfTime() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"verify", "--not-after", "-", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-after", "-", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notAfter(AbstractSopCmd.END_OF_TIME); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { 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()}); + assertUnsupportedOption(() -> + SopCLI.execute("verify", "--not-after", "2019-10-29T18:36:45Z", + signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test public void notBefore_passedDown() throws SOPGPException.UnsupportedOption, ParseException { 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()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-before", "2019-10-29T18:36:45Z", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notBefore(date); } @Test public void notBefore_now() throws SOPGPException.UnsupportedOption { Date now = new Date(); - SopCLI.main(new String[] {"verify", "--not-before", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-before", "now", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notBefore(dateMatcher(now)); } @Test public void notBefore_dashCountsAsBeginningOfTime() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"verify", "--not-before", "-", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-before", "-", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { 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()}); + assertUnsupportedOption(() -> + SopCLI.execute("verify", "--not-before", "2019-10-29T18:36:45Z", + signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test public void notBeforeAndNotAfterAreCalledWithDefaultValues() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notAfter(dateMatcher(new Date())); verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME); } @@ -139,43 +158,43 @@ public class VerifyCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void cert_fileNotFoundCausesExit61() { - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), "invalid.asc"}); + assertMissingInput(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), "invalid.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void cert_badDataCausesExit41() throws SOPGPException.BadData, IOException { when(detachedVerify.cert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void signature_fileNotFoundCausesExit61() { - SopCLI.main(new String[] {"verify", "invalid.sig", cert.getAbsolutePath()}); + assertMissingInput(() -> + SopCLI.execute("verify", "invalid.sig", cert.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void signature_badDataCausesExit41() throws SOPGPException.BadData, IOException { when(detachedVerify.signatures((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.NoSignature.EXIT_CODE) public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.NoSignature()); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertNoSignature(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test @@ -192,7 +211,8 @@ public class VerifyCmdTest { ByteArrayOutputStream out = new ByteArrayOutputStream(); System.setOut(new PrintStream(out)); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); System.setOut(originalSout); diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java index e284e35..92850bd 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java @@ -4,19 +4,19 @@ package sop.cli.picocli.commands; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.SOP; import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; import sop.operation.Version; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption; + public class VersionCmdTest { private Version version; @@ -29,6 +29,8 @@ public class VersionCmdTest { when(version.getVersion()).thenReturn("1.0"); when(version.getExtendedVersion()).thenReturn("MockSop Extended Version Information"); when(version.getBackendVersion()).thenReturn("Foo"); + when(version.getSopSpecVersion()).thenReturn("draft-dkg-openpgp-stateless-cli-XX"); + when(version.getSopVVersion()).thenReturn("1.0"); when(sop.version()).thenReturn(version); SopCLI.setSopInstance(sop); @@ -36,26 +38,41 @@ public class VersionCmdTest { @Test public void assertVersionCommandWorks() { - SopCLI.main(new String[] {"version"}); + assertSuccess(() -> + SopCLI.execute("version")); verify(version, times(1)).getVersion(); verify(version, times(1)).getName(); } @Test public void assertExtendedVersionCommandWorks() { - SopCLI.main(new String[] {"version", "--extended"}); + assertSuccess(() -> + SopCLI.execute("version", "--extended")); verify(version, times(1)).getExtendedVersion(); } @Test public void assertBackendVersionCommandWorks() { - SopCLI.main(new String[] {"version", "--backend"}); + assertSuccess(() -> + SopCLI.execute("version", "--backend")); verify(version, times(1)).getBackendVersion(); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) + public void assertSpecVersionCommandWorks() { + assertSuccess(() -> + SopCLI.execute("version", "--sop-spec")); + } + + @Test + public void assertSOPVVersionCommandWorks() { + assertSuccess(() -> + SopCLI.execute("version", "--sopv")); + } + + @Test public void assertInvalidOptionResultsInExit37() { - SopCLI.main(new String[] {"version", "--invalid"}); + assertUnsupportedOption(() -> + SopCLI.execute("version", "--invalid")); } } diff --git a/sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java b/sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java new file mode 100644 index 0000000..bd07f0b --- /dev/null +++ b/sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.assertions; + +import sop.exception.SOPGPException; + +import java.util.function.IntSupplier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +/** + * DSL for testing the return values of SOP method calls. + */ +public class SopExecutionAssertions { + + /** + * Assert that the execution of the given function returns 0. + * + * @param function function to execute + */ + public static void assertSuccess(IntSupplier function) { + assertEquals(0, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns a generic error with error code 1. + * + * @param function function to execute. + */ + public static void assertGenericError(IntSupplier function) { + assertEquals(1, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns a non-zero error code. + * + * @param function function to execute + */ + public static void assertAnyError(IntSupplier function) { + assertNotEquals(0, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 3 + * (which corresponds to {@link sop.exception.SOPGPException.NoSignature}). + * + * @param function function to execute. + */ + public static void assertNoSignature(IntSupplier function) { + assertEquals(SOPGPException.NoSignature.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 13 + * (which corresponds to {@link sop.exception.SOPGPException.UnsupportedAsymmetricAlgo}). + * + * @param function function to execute. + */ + public static void assertUnsupportedAsymmetricAlgo(IntSupplier function) { + assertEquals(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 17 + * (which corresponds to {@link sop.exception.SOPGPException.CertCannotEncrypt}). + * + * @param function function to execute. + */ + public static void assertCertCannotEncrypt(IntSupplier function) { + assertEquals(SOPGPException.CertCannotEncrypt.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 19 + * (which corresponds to {@link sop.exception.SOPGPException.MissingArg}). + * + * @param function function to execute. + */ + public static void assertMissingArg(IntSupplier function) { + assertEquals(SOPGPException.MissingArg.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 23 + * (which corresponds to {@link sop.exception.SOPGPException.IncompleteVerification}). + * + * @param function function to execute. + */ + public static void assertIncompleteVerification(IntSupplier function) { + assertEquals(SOPGPException.IncompleteVerification.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 29 + * (which corresponds to {@link sop.exception.SOPGPException.CannotDecrypt}). + * + * @param function function to execute. + */ + public static void assertCannotDecrypt(IntSupplier function) { + assertEquals(SOPGPException.CannotDecrypt.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 31 + * (which corresponds to {@link sop.exception.SOPGPException.PasswordNotHumanReadable}). + * + * @param function function to execute. + */ + public static void assertPasswordNotHumanReadable(IntSupplier function) { + assertEquals(SOPGPException.PasswordNotHumanReadable.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 37 + * (which corresponds to {@link sop.exception.SOPGPException.UnsupportedOption}). + * + * @param function function to execute. + */ + public static void assertUnsupportedOption(IntSupplier function) { + assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 41 + * (which corresponds to {@link sop.exception.SOPGPException.BadData}). + * + * @param function function to execute. + */ + public static void assertBadData(IntSupplier function) { + assertEquals(SOPGPException.BadData.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 53 + * (which corresponds to {@link sop.exception.SOPGPException.ExpectedText}). + * + * @param function function to execute. + */ + public static void assertExpectedText(IntSupplier function) { + assertEquals(SOPGPException.ExpectedText.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 59 + * (which corresponds to {@link sop.exception.SOPGPException.OutputExists}). + * + * @param function function to execute. + */ + public static void assertOutputExists(IntSupplier function) { + assertEquals(SOPGPException.OutputExists.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 61 + * (which corresponds to {@link sop.exception.SOPGPException.MissingInput}). + * + * @param function function to execute. + */ + public static void assertMissingInput(IntSupplier function) { + assertEquals(SOPGPException.MissingInput.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 67 + * (which corresponds to {@link sop.exception.SOPGPException.KeyIsProtected}). + * + * @param function function to execute. + */ + public static void assertKeyIsProtected(IntSupplier function) { + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 69 + * (which corresponds to {@link sop.exception.SOPGPException.UnsupportedSubcommand}). + * + * @param function function to execute. + */ + public static void assertUnsupportedSubcommand(IntSupplier function) { + assertEquals(SOPGPException.UnsupportedSubcommand.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 71 + * (which corresponds to {@link sop.exception.SOPGPException.UnsupportedSpecialPrefix}). + * + * @param function function to execute. + */ + public static void assertUnsupportedSpecialPrefix(IntSupplier function) { + assertEquals(SOPGPException.UnsupportedSpecialPrefix.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 73 + * (which corresponds to {@link sop.exception.SOPGPException.AmbiguousInput}). + * + * @param function function to execute. + */ + public static void assertAmbiguousInput(IntSupplier function) { + assertEquals(SOPGPException.AmbiguousInput.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 79 + * (which corresponds to {@link sop.exception.SOPGPException.KeyCannotSign}). + * + * @param function function to execute. + */ + public static void assertKeyCannotSign(IntSupplier function) { + assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 83 + * (which corresponds to {@link sop.exception.SOPGPException.IncompatibleOptions}). + * + * @param function function to execute. + */ + public static void assertIncompatibleOptions(IntSupplier function) { + assertEquals(SOPGPException.IncompatibleOptions.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 89 + * (which corresponds to {@link sop.exception.SOPGPException.UnsupportedProfile}). + * + * @param function function to execute. + */ + public static void assertUnsupportedProfile(IntSupplier function) { + assertEquals(SOPGPException.UnsupportedProfile.EXIT_CODE, function.getAsInt()); + } +} From a13f1e2a0d33e8db43b1d44fbd9e68af43ba7e55 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 27 Mar 2024 21:57:04 +0100 Subject: [PATCH 018/110] Mark ProxyOutputStream as deprecated --- sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt index da6c4fa..a608c89 100644 --- a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt +++ b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt @@ -15,6 +15,7 @@ import java.io.OutputStream * class is useful if we need to provide an [OutputStream] at one point in time when the final * target output stream is not yet known. */ +@Deprecated("Marked for removal.") class ProxyOutputStream : OutputStream() { private val buffer = ByteArrayOutputStream() private var swapped: OutputStream? = null From 93564472263dccc0192f1deec23e9ce10f3014d3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 30 Mar 2024 19:00:09 +0100 Subject: [PATCH 019/110] Remove label() option from armor() operation --- .../kotlin/sop/external/operation/ArmorExternal.kt | 3 --- .../src/main/resources/msg_armor.properties | 1 - .../src/main/resources/msg_armor_de.properties | 1 - sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt | 14 -------------- sop-java/src/main/kotlin/sop/operation/Armor.kt | 12 ------------ 5 files changed, 31 deletions(-) delete mode 100644 sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt diff --git a/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt index f80c57b..b202746 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt @@ -7,7 +7,6 @@ package sop.external.operation import java.io.InputStream import java.util.Properties import sop.Ready -import sop.enums.ArmorLabel import sop.exception.SOPGPException import sop.external.ExternalSOP import sop.operation.Armor @@ -18,8 +17,6 @@ class ArmorExternal(binary: String, environment: Properties) : Armor { private val commandList: MutableList = mutableListOf(binary, "armor") private val envList: List = ExternalSOP.propertiesToEnv(environment) - override fun label(label: ArmorLabel): Armor = apply { commandList.add("--label=$label") } - @Throws(SOPGPException.BadData::class) override fun data(data: InputStream): Ready = ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) diff --git a/sop-java-picocli/src/main/resources/msg_armor.properties b/sop-java-picocli/src/main/resources/msg_armor.properties index 2f4e217..b4dcb59 100644 --- a/sop-java-picocli/src/main/resources/msg_armor.properties +++ b/sop-java-picocli/src/main/resources/msg_armor.properties @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: Apache-2.0 usage.header=Add ASCII Armor to standard input -label=Label to be used in the header and tail of the armoring stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 diff --git a/sop-java-picocli/src/main/resources/msg_armor_de.properties b/sop-java-picocli/src/main/resources/msg_armor_de.properties index a2303e9..4c365a8 100644 --- a/sop-java-picocli/src/main/resources/msg_armor_de.properties +++ b/sop-java-picocli/src/main/resources/msg_armor_de.properties @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: Apache-2.0 usage.header=Schütze Standard-Eingabe mit ASCII Armor -label=Label für Kopf- und Fußzeile der ASCII Armor stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 diff --git a/sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt b/sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt deleted file mode 100644 index 8b4e2cd..0000000 --- a/sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums - -@Deprecated("Use of armor labels is deprecated.") -enum class ArmorLabel { - auto, - sig, - key, - cert, - message -} diff --git a/sop-java/src/main/kotlin/sop/operation/Armor.kt b/sop-java/src/main/kotlin/sop/operation/Armor.kt index e89708b..be7f1a3 100644 --- a/sop-java/src/main/kotlin/sop/operation/Armor.kt +++ b/sop-java/src/main/kotlin/sop/operation/Armor.kt @@ -7,22 +7,10 @@ package sop.operation import java.io.IOException import java.io.InputStream import sop.Ready -import sop.enums.ArmorLabel import sop.exception.SOPGPException.BadData -import sop.exception.SOPGPException.UnsupportedOption interface Armor { - /** - * Overrides automatic detection of label. - * - * @param label armor label - * @return builder instance - */ - @Deprecated("Use of armor labels is deprecated and will be removed in a future release.") - @Throws(UnsupportedOption::class) - fun label(label: ArmorLabel): Armor - /** * Armor the provided data. * From 1d80ff1d8d24a4a2f5efae1289a6c6b0da09a0da Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 30 Mar 2024 19:02:58 +0100 Subject: [PATCH 020/110] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 819523e..361d458 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 10.0.1-SNAPSHOT +- Remove `label()` option from `Armor` operation + ## 10.0.0 - Update implementation to [SOP Specification revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html). - Throw `BadData` when passing KEYS where CERTS are expected From d5c011ea4a429bbb0ac2b38b609c7235356caf6c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 9 Jul 2024 14:29:22 +0200 Subject: [PATCH 021/110] Add new SOPGPException types related to hardware modules --- .../kotlin/sop/exception/SOPGPException.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt index 2473258..bc9131f 100644 --- a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt +++ b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt @@ -305,4 +305,36 @@ abstract class SOPGPException : RuntimeException { const val EXIT_CODE = 89 } } + + /** + * The sop implementation supports some form of hardware-backed secret keys, but could not + * identify the hardware device. + */ + class NoHardwareKeyFound : SOPGPException { + constructor() : super() + + constructor(errorMsg: String) : super(errorMsg) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 97 + } + } + + /** + * The sop implementation tried to use a hardware-backed secret key, but the cryptographic + * hardware refused the operation for some reason other than a bad PIN or password. + */ + class HardwareKeyFailure : SOPGPException { + constructor() : super() + + constructor(errorMsg: String) : super(errorMsg) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 101 + } + } } From ec90d2e951a65d5d947846826e7d0bd2b2333089 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 9 Jul 2024 14:39:03 +0200 Subject: [PATCH 022/110] Add translations for new hardware exception error messages --- sop-java-picocli/src/main/resources/msg_sop.properties | 2 ++ sop-java-picocli/src/main/resources/msg_sop_de.properties | 2 ++ 2 files changed, 4 insertions(+) diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 7979eb3..94e4dc0 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -36,6 +36,8 @@ usage.exitCodeList.17=73:Ambiguous input (a filename matching the designator alr usage.exitCodeList.18=79:Key is not signing capable usage.exitCodeList.19=83:Options were supplied that are incompatible with each other usage.exitCodeList.20=89:The requested profile is unsupported, or the indicated subcommand does not accept profiles +usage.exitCodeList.21=97:The implementation supports some form of hardware-backed secret keys, but could not identify the hardware device +usage.exitCodeList.22=101:The implementation tried to use a hardware-backed secret key, but the cryptographic hardware refused the operation for some reason other than a bad PIN or password ## SHARED RESOURCES stacktrace=Print stacktrace diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 40a316d..786fa36 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -36,6 +36,8 @@ usage.exitCodeList.17=73:Mehrdeutige Eingabe (ein Dateiname, der dem Bezeichner usage.exitCodeList.18=79:Schlüssel ist nicht fähig zu signieren usage.exitCodeList.19=83:Miteinander inkompatible Optionen spezifiziert usage.exitCodeList.20=89:Das angeforderte Profil wird nicht unterstützt, oder der angegebene Unterbefehl akzeptiert keine Profile +usage.exitCodeList.21=97:Die Anwendung unterstützt hardwaregestützte private Schlüssel, aber kann das Gerät nicht identifizieren +usage.exitCodeList.22=101:Die Anwendung versuchte, einen hardwaregestützten Schlüssel zu verwenden, aber das Gerät lehnte den Vorgang aus einem anderen Grund als einer falschen PIN oder einem falschen Passwort ab ## SHARED RESOURCES stacktrace=Stacktrace ausgeben From ce21e07fc2a7d45655a84a7c70534ba5205896d6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 19:20:33 +0200 Subject: [PATCH 023/110] Properly match MissingArg exception code --- .../main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt index 29aa77b..5778bb9 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt @@ -21,6 +21,8 @@ class SOPExceptionExitCodeMapper : IExitCodeExceptionMapper { // Unmatched subcommand SOPGPException.UnsupportedSubcommand.EXIT_CODE } + } else if (exception is MissingParameterException) { + SOPGPException.MissingArg.EXIT_CODE } else if (exception is ParameterException) { // Invalid option (e.g. `--as invalid`) SOPGPException.UnsupportedOption.EXIT_CODE From 8422a23396cda5d46dd0940bbaeaaf100b54caa3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 19:25:38 +0200 Subject: [PATCH 024/110] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 361d458..ef20557 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ SPDX-License-Identifier: Apache-2.0 ## 10.0.1-SNAPSHOT - Remove `label()` option from `Armor` operation +- Fix exit code for 'Missing required option/parameter' error ## 10.0.0 - Update implementation to [SOP Specification revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html). From ed9fb3154927b7f1a92931509f97643ae2d8be79 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 19:38:59 +0200 Subject: [PATCH 025/110] Fix woodpecker warnings --- .woodpecker/build.yml | 2 ++ .woodpecker/reuse.yml | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index ff59c4e..fab075a 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -1,5 +1,7 @@ steps: run: + when: + event: push image: gradle:7.6-jdk11-jammy commands: # Install Sequoia-SOP diff --git a/.woodpecker/reuse.yml b/.woodpecker/reuse.yml index d78c61e..b278a39 100644 --- a/.woodpecker/reuse.yml +++ b/.woodpecker/reuse.yml @@ -2,6 +2,8 @@ # See https://reuse.software/ steps: reuse: + when: + event: push image: fsfe/reuse:latest commands: - - reuse lint \ No newline at end of file + - reuse lint From 61f3bf14c5c01582699501fdec5ce5a82c4d33ac Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:00:30 +0200 Subject: [PATCH 026/110] Fix NPE in DecryptExternal when reading lines --- .../main/kotlin/sop/external/operation/DecryptExternal.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt index b68d3a6..e0a900d 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt @@ -108,8 +108,8 @@ class DecryptExternal( finish(process) val sessionKeyOutIn = FileInputStream(sessionKeyOut) - var line = readString(sessionKeyOutIn) - val sessionKey = SessionKey.fromString(line.trim { it <= ' ' }) + var line: String? = readString(sessionKeyOutIn) + val sessionKey = line?.let { l -> SessionKey.fromString(l.trim { it <= ' ' }) } sessionKeyOutIn.close() sessionKeyOut.delete() @@ -118,7 +118,7 @@ class DecryptExternal( val verifyOutIn = FileInputStream(verifyOut) val reader = BufferedReader(InputStreamReader(verifyOutIn)) while (reader.readLine().also { line = it } != null) { - verifications.add(Verification.fromString(line.trim())) + line?.let { verifications.add(Verification.fromString(it.trim())) } } reader.close() } From 8843d217e41875436ebec8ee8b8bc519c39ca295 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:13:12 +0200 Subject: [PATCH 027/110] revoke-key command: Allow for multiple '--with-key-password' options --- .../main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt index 0b93ac5..b9b1015 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt @@ -19,8 +19,8 @@ class RevokeKeyCmd : AbstractSopCmd() { @Option(names = ["--no-armor"], negatable = true) var armor = true - @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") - var withKeyPassword: String? = null + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD", arity = "0..*") + var withKeyPassword: List = listOf() override fun run() { val revokeKey = throwIfUnsupportedSubcommand(SopCLI.getSop().revokeKey(), "revoke-key") @@ -29,9 +29,9 @@ class RevokeKeyCmd : AbstractSopCmd() { revokeKey.noArmor() } - withKeyPassword?.let { + for (passwordIn in withKeyPassword) { try { - val password = stringFromInputStream(getInput(it)) + val password = stringFromInputStream(getInput(passwordIn)) revokeKey.withKeyPassword(password) } catch (e: SOPGPException.UnsupportedOption) { val errorMsg = From 8134e3cd12d0bbd1dbf79c3a2f3f6d4dfffc415a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:16:11 +0200 Subject: [PATCH 028/110] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef20557..2c266f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ SPDX-License-Identifier: Apache-2.0 ## 10.0.1-SNAPSHOT - Remove `label()` option from `Armor` operation - Fix exit code for 'Missing required option/parameter' error +- Fix `revoke-key`: Allow for multiple invocations of `--with-key-password` option ## 10.0.0 - Update implementation to [SOP Specification revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html). From 5f18edd2e76b56c478fc7565cb2dd3e6de47714e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:37:46 +0200 Subject: [PATCH 029/110] EncryptExternal: Fix parameter passing for --sign-with option --- .../src/main/kotlin/sop/external/operation/EncryptExternal.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt index 6f1cc6c..12d9cff 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt @@ -36,7 +36,7 @@ class EncryptExternal( override fun mode(mode: EncryptAs): Encrypt = apply { commandList.add("--as=$mode") } override fun signWith(key: InputStream): Encrypt = apply { - commandList.add("--sign-with@ENV:SIGN_WITH_$argCounter") + commandList.add("--sign-with=@ENV:SIGN_WITH_$argCounter") envList.add("SIGN_WITH_$argCounter=${ExternalSOP.readString(key)}") argCounter += 1 } From a07446e29a963190ce142b346c7825d91ad87eb7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Sep 2024 22:40:36 +0200 Subject: [PATCH 030/110] Add new exceptions --- .../src/main/resources/msg_sop.properties | 2 ++ .../src/main/resources/msg_sop_de.properties | 2 ++ .../kotlin/sop/exception/SOPGPException.kt | 30 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 94e4dc0..8179676 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -38,6 +38,8 @@ usage.exitCodeList.19=83:Options were supplied that are incompatible with each o usage.exitCodeList.20=89:The requested profile is unsupported, or the indicated subcommand does not accept profiles usage.exitCodeList.21=97:The implementation supports some form of hardware-backed secret keys, but could not identify the hardware device usage.exitCodeList.22=101:The implementation tried to use a hardware-backed secret key, but the cryptographic hardware refused the operation for some reason other than a bad PIN or password +usage.exitCodeList.23=103:The primary key of a KEYS object is too weak or revoked +usage.exitCodeList.24=107:The CERTS object has no matching User ID ## SHARED RESOURCES stacktrace=Print stacktrace diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 786fa36..0538cd9 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -38,6 +38,8 @@ usage.exitCodeList.19=83:Miteinander inkompatible Optionen spezifiziert usage.exitCodeList.20=89:Das angeforderte Profil wird nicht unterstützt, oder der angegebene Unterbefehl akzeptiert keine Profile usage.exitCodeList.21=97:Die Anwendung unterstützt hardwaregestützte private Schlüssel, aber kann das Gerät nicht identifizieren usage.exitCodeList.22=101:Die Anwendung versuchte, einen hardwaregestützten Schlüssel zu verwenden, aber das Gerät lehnte den Vorgang aus einem anderen Grund als einer falschen PIN oder einem falschen Passwort ab +usage.exitCodeList.23=103:Der primäre private Schlüssel ist zu schwach oder widerrufen +usage.exitCodeList.24=107:Das Zertifikat hat keine übereinstimmende User ID ## SHARED RESOURCES stacktrace=Stacktrace ausgeben diff --git a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt index bc9131f..1f9ce6b 100644 --- a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt +++ b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt @@ -337,4 +337,34 @@ abstract class SOPGPException : RuntimeException { const val EXIT_CODE = 101 } } + + /** + * The primary key of a KEYS object is too weak or revoked. + */ + class PrimaryKeyBad : SOPGPException { + constructor() : super() + + constructor(errorMsg: String) : super(errorMsg) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 103 + } + } + + /** + * The CERTS object has no matching User ID. + */ + class CertUserIdNoMatch : SOPGPException { + constructor() : super() + + constructor(errorMsg: String) : super(errorMsg) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 107 + } + } } From a2315f9847b123b05fafccd3263455565a469641 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Sep 2024 22:43:36 +0200 Subject: [PATCH 031/110] Add implementation of update-key command --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 1 + .../sop/cli/picocli/commands/UpdateKeyCmd.kt | 76 +++++++++++++++++++ .../main/resources/msg_update-key.properties | 19 +++++ .../resources/msg_update-key_de.properties | 18 +++++ sop-java/src/main/kotlin/sop/SOP.kt | 18 ++--- .../main/kotlin/sop/operation/UpdateKey.kt | 43 +++++++++++ 6 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt create mode 100644 sop-java-picocli/src/main/resources/msg_update-key.properties create mode 100644 sop-java-picocli/src/main/resources/msg_update-key_de.properties create mode 100644 sop-java/src/main/kotlin/sop/operation/UpdateKey.kt diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 1d5d46b..056aa27 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -27,6 +27,7 @@ import sop.exception.SOPGPException ChangeKeyPasswordCmd::class, RevokeKeyCmd::class, ExtractCertCmd::class, + UpdateKeyCmd::class, // Messaging subcommands SignCmd::class, VerifyCmd::class, diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt new file mode 100644 index 0000000..2afa015 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.* +import java.io.IOException + +@Command( + name = "update-key", + resourceBundle = "msg_update-key", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class UpdateKeyCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--signing-only"]) var signingOnly = false + + @Option(names = ["--no-new-mechanisms"]) var noNewMechanisms = false + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + @Option(names = ["--merge-certs"], paramLabel = "CERTS") + var mergeCerts: List = listOf() + + override fun run() { + val updateKey = throwIfUnsupportedSubcommand(SopCLI.getSop().updateKey(), "update-key") + + if (!armor) { + updateKey.noArmor() + } + + if (signingOnly) { + updateKey.signingOnly() + } + + if (noNewMechanisms) { + updateKey.noNewMechanisms() + } + + for (passwordFileName in withKeyPassword) { + try { + val password = stringFromInputStream(getInput(passwordFileName)) + updateKey.withKeyPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + for (certInput in mergeCerts) { + try { + getInput(certInput).use { certIn -> updateKey.mergeCerts(certIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput) + throw BadData(errorMsg, badData) + } + } + + try { + val ready = updateKey.key(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} \ No newline at end of file diff --git a/sop-java-picocli/src/main/resources/msg_update-key.properties b/sop-java-picocli/src/main/resources/msg_update-key.properties new file mode 100644 index 0000000..dd4446d --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_update-key.properties @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Keep a secret key up-to-date +no-armor=ASCII armor the output +signing-only=TODO: Document +no-new-mechanisms=Do not add feature support for new mechanisms, which the key did not previously support +with-key-password.0=Passphrase to unlock the secret key(s). +with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +merge-certs.0=Merge additional elements found in the corresponding CERTS objects into the updated secret keys +merge-certs.1=This can be used, for example, to absorb a third-party certification into the Transferable Secret Key + +stacktrace=Print stacktrace +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=%nParameters:%n +usage.synopsisHeading=Usage:\u0020 +usage.commandListHeading = %nCommands:%n +usage.optionListHeading = %nOptions:%n +usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_update-key_de.properties b/sop-java-picocli/src/main/resources/msg_update-key_de.properties new file mode 100644 index 0000000..86b999e --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_update-key_de.properties @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Halte einen Schlüssel auf dem neusten Stand +no-armor=Schütze Ausgabe mit ASCII Armor +signing-only=TODO: Dokumentieren +no-new-mechanisms=Füge keine neuen Funktionen hinzu, die der Schlüssel nicht bereits zuvor unterstützt hat +with-key-password.0=Passwort zum Entsperren der privaten Schlüssel +with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +merge-certs.0=Führe zusätzliche Elemente aus entsprechenden CERTS Objekten mit dem privaten Schlüssel zusammen +merge-certs.1=Dies kann zum Beispiel dazu genutzt werden, Zertifizierungen dritter in den privaten Schlüssel zu übernehmen + +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=%nParameter:%n +usage.synopsisHeading=Aufruf:\u0020 +usage.commandListHeading=%nBefehle:%n +usage.optionListHeading = %nOptionen:%n +usage.footerHeading=Powered by Picocli%n diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index 7fdd414..c53bb7d 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -4,18 +4,7 @@ package sop -import sop.operation.Armor -import sop.operation.ChangeKeyPassword -import sop.operation.Dearmor -import sop.operation.Decrypt -import sop.operation.DetachedSign -import sop.operation.Encrypt -import sop.operation.ExtractCert -import sop.operation.GenerateKey -import sop.operation.InlineDetach -import sop.operation.InlineSign -import sop.operation.ListProfiles -import sop.operation.RevokeKey +import sop.operation.* /** * Stateless OpenPGP Interface. This class provides a stateless interface to various OpenPGP related @@ -70,4 +59,9 @@ interface SOP : SOPV { /** Update a key's password. */ fun changeKeyPassword(): ChangeKeyPassword + + /** + * Keep a secret key up-to-date. + */ + fun updateKey(): UpdateKey } diff --git a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt new file mode 100644 index 0000000..1b12f6f --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import sop.Ready +import sop.exception.SOPGPException +import sop.util.UTF8Util +import java.io.IOException +import java.io.InputStream + +interface UpdateKey { + + /** + * Disable ASCII armor encoding of the output. + * + * @return builder instance + */ + fun noArmor(): UpdateKey + + @Throws(SOPGPException.UnsupportedOption::class) fun signingOnly(): UpdateKey + + @Throws(SOPGPException.UnsupportedOption::class) fun noNewMechanisms(): UpdateKey + + @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + fun withKeyPassword(password: String): UpdateKey = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + + @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + fun withKeyPassword(password: ByteArray): UpdateKey + + @Throws(SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) + fun mergeCerts(certs: InputStream): UpdateKey + + @Throws(SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) + fun mergeCerts(certs: ByteArray): UpdateKey = mergeCerts(certs.inputStream()) + + @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class, SOPGPException.PrimaryKeyBad::class) + fun key(key: InputStream): Ready + + @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class, SOPGPException.PrimaryKeyBad::class) + fun key(key: ByteArray): Ready = key(key.inputStream()) +} \ No newline at end of file From ddf4ba19f9c80c801afbf2ecb2b8b79ded5abe3d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Sep 2024 22:43:50 +0200 Subject: [PATCH 032/110] Add implementation of merge-certs command --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 1 + .../sop/cli/picocli/commands/MergeCertsCmd.kt | 48 +++++++++++++++++++ .../main/resources/msg_merge-certs.properties | 15 ++++++ .../resources/msg_merge-certs_de.properties | 19 ++++++++ sop-java/src/main/kotlin/sop/SOP.kt | 5 ++ .../main/kotlin/sop/operation/MergeCerts.kt | 28 +++++++++++ 6 files changed, 116 insertions(+) create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt create mode 100644 sop-java-picocli/src/main/resources/msg_merge-certs.properties create mode 100644 sop-java-picocli/src/main/resources/msg_merge-certs_de.properties create mode 100644 sop-java/src/main/kotlin/sop/operation/MergeCerts.kt diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 056aa27..3aa5fee 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -28,6 +28,7 @@ import sop.exception.SOPGPException RevokeKeyCmd::class, ExtractCertCmd::class, UpdateKeyCmd::class, + MergeCertsCmd::class, // Messaging subcommands SignCmd::class, VerifyCmd::class, diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt new file mode 100644 index 0000000..15b33f8 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import picocli.CommandLine +import picocli.CommandLine.Command +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import java.io.IOException + +@Command( + name = "merge-certs", + resourceBundle = "msg_merge-certs", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class MergeCertsCmd : AbstractSopCmd() { + + @CommandLine.Option(names = ["--no-armor"], negatable = true) + var armor = false + + @CommandLine.Parameters(paramLabel = "CERTS") + var updates: List = listOf() + + override fun run() { + val mergeCerts = throwIfUnsupportedSubcommand(SopCLI.getSop().mergeCerts(), "merge-certs") + + if (!armor) { + mergeCerts.noArmor() + } + + for (certFileName in updates) { + try { + getInput(certFileName).use { mergeCerts.updates(it) } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + try { + + val ready = mergeCerts.baseCertificates(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} \ No newline at end of file diff --git a/sop-java-picocli/src/main/resources/msg_merge-certs.properties b/sop-java-picocli/src/main/resources/msg_merge-certs.properties new file mode 100644 index 0000000..866db4b --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_merge-certs.properties @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.headerHeading=Merge OpenPGP certificates%n +usage.description=BLABLA +no-armor=ASCII armor the output +CERTS[0..*]=OpenPGP certificates from which updates shall be merged into the base certificates from standard input + +stacktrace=Print stacktrace +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=%nParameters:%n +usage.synopsisHeading=Usage:\u0020 +usage.commandListHeading = %nCommands:%n +usage.optionListHeading = %nOptions:%n +usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties b/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties new file mode 100644 index 0000000..021c535 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.headerHeading=OpenPGP Zertifikate zusammenführen%n%n +usage.header=Führe OpenPGP Zertifikate aus der Standardeingabe mit ensprechenden Elementen aus CERTS zusammen und gebe das Ergebnis auf der Standardausgabe aus +usage.description=Es werden nur Zertifikate auf die Standardausgabe geschrieben, welche Teil der Standardeingabe waren +no-armor=Schütze Ausgabe mit ASCII Armor +CERTS[0..*]=OpenPGP Zertifikate aus denen neue Elemente in die Basiszertifikate aus der Standardeingabe übernommen werden sollen + +usage.parameterList.0=STANDARDIN +usage.parameterList.1=STANDARDOUT + +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=%nParameter:%n +usage.synopsisHeading=Aufruf:\u0020 +usage.descriptionHeading=%nHinweise:%n +usage.commandListHeading=%nBefehle:%n +usage.optionListHeading = %nOptionen:%n +usage.footerHeading=Powered by Picocli%n diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index c53bb7d..1640d5f 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -64,4 +64,9 @@ interface SOP : SOPV { * Keep a secret key up-to-date. */ fun updateKey(): UpdateKey + + /** + * Merge OpenPGP certificates. + */ + fun mergeCerts(): MergeCerts } diff --git a/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt new file mode 100644 index 0000000..f60d291 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import sop.Ready +import sop.exception.SOPGPException +import java.io.IOException +import java.io.InputStream + +interface MergeCerts { + + @Throws(SOPGPException.UnsupportedOption::class) + fun noArmor(): MergeCerts + + @Throws(SOPGPException.BadData::class, IOException::class) + fun updates(updateCerts: InputStream): MergeCerts + + @Throws(SOPGPException.BadData::class, IOException::class) + fun updates(updateCerts: ByteArray): MergeCerts = updates(updateCerts.inputStream()) + + @Throws(SOPGPException.BadData::class, IOException::class) + fun baseCertificates(certs: InputStream): Ready + + @Throws(SOPGPException.BadData::class, IOException::class) + fun baseCertificates(certs: ByteArray): Ready = baseCertificates(certs.inputStream()) +} \ No newline at end of file From a607013cfb727d74e97b564d8c01f45552d5a0f5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 15:50:17 +0200 Subject: [PATCH 033/110] Add support for rendering help info for input and output --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 14 ++- .../cli/picocli/commands/AbstractSopCmd.kt | 100 ++++++++++++++++++ .../src/main/resources/msg_sop.properties | 7 +- .../src/main/resources/msg_sop_de.properties | 5 +- 4 files changed, 120 insertions(+), 6 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 3aa5fee..4b82c81 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -81,8 +81,8 @@ class SopCLI { // Re-set bundle with updated locale cliMsg = ResourceBundle.getBundle("msg_sop") - return CommandLine(SopCLI::class.java) - .apply { + val cmd = + CommandLine(SopCLI::class.java).apply { // explicitly set help command resource bundle subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help")) // Hide generate-completion command @@ -94,7 +94,15 @@ class SopCLI { exitCodeExceptionMapper = SOPExceptionExitCodeMapper() isCaseInsensitiveEnumValuesAllowed = true } - .execute(*args) + + // render Input/Output sections in help command + cmd.subcommands.values + .filter { + (it.getCommand() as Any) is AbstractSopCmd + } // Only for AbstractSopCmd objects + .forEach { (it.getCommand() as AbstractSopCmd).installIORenderer(it) } + + return cmd.execute(*args) } } diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt index 4629e57..65be1be 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt @@ -7,6 +7,11 @@ package sop.cli.picocli.commands import java.io.* import java.text.ParseException import java.util.* +import picocli.CommandLine +import picocli.CommandLine.Help +import picocli.CommandLine.Help.Column +import picocli.CommandLine.Help.TextTable +import picocli.CommandLine.IHelpSectionRenderer import sop.cli.picocli.commands.AbstractSopCmd.EnvironmentVariableResolver import sop.exception.SOPGPException.* import sop.util.UTCUtil.Companion.parseUTCDate @@ -215,11 +220,106 @@ abstract class AbstractSopCmd(locale: Locale = Locale.getDefault()) : Runnable { } } + /** + * See + * [Example](https://github.com/remkop/picocli/blob/main/picocli-examples/src/main/java/picocli/examples/customhelp/EnvironmentVariablesSection.java) + */ + class InputOutputHelpSectionRenderer(private val argument: Pair) : + IHelpSectionRenderer { + + override fun render(help: Help): String { + return argument.let { + val calcLen = + help.calcLongOptionColumnWidth( + help.commandSpec().options(), + help.commandSpec().positionalParameters(), + help.colorScheme()) + val keyLength = + help + .commandSpec() + .usageMessage() + .longOptionsMaxWidth() + .coerceAtMost(calcLen - 1) + val table = + TextTable.forColumns( + help.colorScheme(), + Column(keyLength + 7, 6, Column.Overflow.SPAN), + Column(width(help) - (keyLength + 7), 0, Column.Overflow.WRAP)) + table.setAdjustLineBreaksForWideCJKCharacters(adjustCJK(help)) + table.addRowValues("@|yellow ${argument.first}|@", argument.second ?: "") + table.toString() + } + } + + private fun adjustCJK(help: Help) = + help.commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters() + + private fun width(help: Help) = help.commandSpec().usageMessage().width() + } + + fun installIORenderer(cmd: CommandLine) { + val inputName = getResString(cmd, "standardInput") + if (inputName != null) { + cmd.helpSectionMap[SECTION_KEY_STANDARD_INPUT_HEADING] = IHelpSectionRenderer { + getResString(cmd, "standardInputHeading") + } + cmd.helpSectionMap[SECTION_KEY_STANDARD_INPUT_DETAILS] = + InputOutputHelpSectionRenderer( + inputName to getResString(cmd, "standardInputDescription")) + cmd.helpSectionKeys = + insertKey( + cmd.helpSectionKeys, + SECTION_KEY_STANDARD_INPUT_HEADING, + SECTION_KEY_STANDARD_INPUT_DETAILS) + } + + val outputName = getResString(cmd, "standardOutput") + if (outputName != null) { + cmd.helpSectionMap[SECTION_KEY_STANDARD_OUTPUT_HEADING] = IHelpSectionRenderer { + getResString(cmd, "standardOutputHeading") + } + cmd.helpSectionMap[SECTION_KEY_STANDARD_OUTPUT_DETAILS] = + InputOutputHelpSectionRenderer( + outputName to getResString(cmd, "standardOutputDescription")) + cmd.helpSectionKeys = + insertKey( + cmd.helpSectionKeys, + SECTION_KEY_STANDARD_OUTPUT_HEADING, + SECTION_KEY_STANDARD_OUTPUT_DETAILS) + } + } + + private fun insertKey(keys: List, header: String, details: String): List { + val index = + keys.indexOf(CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST_HEADING) + val result = keys.toMutableList() + result.add(index, header) + result.add(index + 1, details) + return result + } + + private fun getResString(cmd: CommandLine, key: String): String? = + try { + cmd.resourceBundle.getString(key) + } catch (m: MissingResourceException) { + try { + cmd.parent.resourceBundle.getString(key) + } catch (m: MissingResourceException) { + null + } + } + ?.let { String.format(it) } + companion object { const val PRFX_ENV = "@ENV:" const val PRFX_FD = "@FD:" + const val SECTION_KEY_STANDARD_INPUT_HEADING = "standardInputHeading" + const val SECTION_KEY_STANDARD_INPUT_DETAILS = "standardInput" + const val SECTION_KEY_STANDARD_OUTPUT_HEADING = "standardOutputHeading" + const val SECTION_KEY_STANDARD_OUTPUT_DETAILS = "standardOutput" + @JvmField val DAWN_OF_TIME = Date(0) @JvmField diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 8179676..d5d997a 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -9,10 +9,13 @@ locale=Locale for description texts # Generic usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n +standardInputHeading=%nInput:%n +standardOutputHeading=%nOutput:%n + # Exit Codes usage.exitCodeListHeading=%nExit Codes:%n usage.exitCodeList.0=\u00200:Successful program execution diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 0538cd9..73efe89 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -10,9 +10,12 @@ locale=Gebietsschema f # Generic usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n +standardInputHeading=%nEingabe:%n +standardOutputHeading=%nAusgabe:%n + # Exit Codes usage.exitCodeListHeading=%nExit Codes:%n usage.exitCodeList.0=\u00200:Erfolgreiche Programmausführung From 3104085fe715f359b77d1fb4f148117f57dc1ebc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 16:01:30 +0200 Subject: [PATCH 034/110] Implement certify-userid command --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 1 + .../cli/picocli/commands/CertifyUserIdCmd.kt | 87 +++++++++++++++++++ .../resources/msg_certify-userid.properties | 23 +++++ .../msg_certify-userid_de.properties | 20 +++++ sop-java/src/main/kotlin/sop/SOP.kt | 5 ++ .../kotlin/sop/operation/CertifyUserId.kt | 41 +++++++++ 6 files changed, 177 insertions(+) create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt create mode 100644 sop-java-picocli/src/main/resources/msg_certify-userid.properties create mode 100644 sop-java-picocli/src/main/resources/msg_certify-userid_de.properties create mode 100644 sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 4b82c81..ca62038 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -29,6 +29,7 @@ import sop.exception.SOPGPException ExtractCertCmd::class, UpdateKeyCmd::class, MergeCertsCmd::class, + CertifyUserIdCmd::class, // Messaging subcommands SignCmd::class, VerifyCmd::class, diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt new file mode 100644 index 0000000..71ef79f --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import picocli.CommandLine.Model.CommandSpec +import picocli.CommandLine.Option +import picocli.CommandLine.Parameters +import picocli.CommandLine.Spec +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.UnsupportedOption + +@Command( + name = "certify-userid", + resourceBundle = "msg_certify-userid", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class CertifyUserIdCmd : AbstractSopCmd() { + + @Spec var spec: CommandSpec? = null + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--userid"], required = true, arity = "1..*", paramLabel = "USERID") + var userIds: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + @Option(names = ["--no-require-self-sig"]) var noRequireSelfSig = false + + @Parameters(paramLabel = "KEYS", arity = "1..*") var keys: List = listOf() + + override fun run() { + val certifyUserId = + throwIfUnsupportedSubcommand(SopCLI.getSop().certifyUserId(), "certify-userid") + + if (!armor) { + certifyUserId.noArmor() + } + + if (noRequireSelfSig) { + certifyUserId.noRequireSelfSig() + } + + for (userId in userIds) { + certifyUserId.userId(userId) + } + + for (passwordFileName in withKeyPassword) { + try { + val password = stringFromInputStream(getInput(passwordFileName)) + certifyUserId.withKeyPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + for (keyInput in keys) { + try { + getInput(keyInput).use { certifyUserId.keys(it) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput) + throw BadData(errorMsg, badData) + } + } + + try { + val ready = certifyUserId.certs(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", "STDIN") + throw BadData(errorMsg, badData) + } + } +} diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid.properties b/sop-java-picocli/src/main/resources/msg_certify-userid.properties new file mode 100644 index 0000000..5eb7aa3 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_certify-userid.properties @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Certify OpenPGP Certificate User IDs +no-armor=ASCII armor the output +userid=Identities that shall be certified +with-key-password.0=Passphrase to unlock the secret key(s). +with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +no-require-self-sig=Certify the UserID regardless of whether self-certifications are present +KEYS[0..*]=Private keys + +standardInput=CERTS +standardInputDescription=Certificates that shall be certified +standardOutput=CERTS +standardOutputDescription=Certified certificates + +stacktrace=Print stacktrace +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=Parameters:%n +usage.synopsisHeading=Usage:\u0020 +usage.commandListHeading = Commands:%n +usage.optionListHeading = Options:%n +usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties new file mode 100644 index 0000000..0237fa6 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Zertifiziere OpenPGP Zertifikat Identitäten +no-armor=Schütze Ausgabe mit ASCII Armor +userid=Identität, die zertifiziert werden soll +with-key-password.0=Passwort zum Entsperren der privaten Schlüssel +with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +no-require-self-sig=Zertifiziere die Identität, unabhängig davon, ob eine Selbstzertifizierung vorhanden ist +KEYS[0..*]=Private Schlüssel + +standardInputDescription=Zertifikate, auf denen Identitäten zertifiziert werden sollen +standardOutputDescription=Zertifizierte Zertifikate + +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=Parameter:%n +usage.synopsisHeading=Aufruf:\u0020 +usage.commandListHeading=Befehle:%n +usage.optionListHeading = Optionen:%n +usage.footerHeading=Powered by Picocli%n diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index 1640d5f..5435cad 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -69,4 +69,9 @@ interface SOP : SOPV { * Merge OpenPGP certificates. */ fun mergeCerts(): MergeCerts + + /** + * Certify OpenPGP Certificate User-IDs. + */ + fun certifyUserId(): CertifyUserId } diff --git a/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt new file mode 100644 index 0000000..92fff20 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import sop.Ready +import sop.exception.SOPGPException +import sop.util.UTF8Util +import java.io.IOException +import java.io.InputStream + +interface CertifyUserId { + + @Throws(SOPGPException.UnsupportedOption::class) + fun noArmor(): CertifyUserId + + @Throws(SOPGPException.UnsupportedOption::class) + fun userId(userId: String): CertifyUserId + + @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + fun withKeyPassword(password: String): CertifyUserId = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + + @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + fun withKeyPassword(password: ByteArray): CertifyUserId + + @Throws(SOPGPException.UnsupportedOption::class) + fun noRequireSelfSig(): CertifyUserId + + @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class) + fun keys(keys: InputStream): CertifyUserId + + @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class) + fun keys(keys: ByteArray): CertifyUserId = keys(keys.inputStream()) + + @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + fun certs(certs: InputStream): Ready + + @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + fun certs(certs: ByteArray): Ready = certs(certs.inputStream()) +} \ No newline at end of file From 5e1915c3ceb4e10e9a469a4638c627b4879d4046 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 16:01:58 +0200 Subject: [PATCH 035/110] Checkstyle and exception handling improvements --- .../sop/cli/picocli/commands/MergeCertsCmd.kt | 10 ++++------ .../sop/cli/picocli/commands/UpdateKeyCmd.kt | 15 +++++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt index 15b33f8..16d56e3 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt @@ -4,11 +4,11 @@ package sop.cli.picocli.commands +import java.io.IOException import picocli.CommandLine import picocli.CommandLine.Command import sop.cli.picocli.SopCLI import sop.exception.SOPGPException -import java.io.IOException @Command( name = "merge-certs", @@ -16,11 +16,9 @@ import java.io.IOException exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) class MergeCertsCmd : AbstractSopCmd() { - @CommandLine.Option(names = ["--no-armor"], negatable = true) - var armor = false + @CommandLine.Option(names = ["--no-armor"], negatable = true) var armor = false - @CommandLine.Parameters(paramLabel = "CERTS") - var updates: List = listOf() + @CommandLine.Parameters(paramLabel = "CERTS") var updates: List = listOf() override fun run() { val mergeCerts = throwIfUnsupportedSubcommand(SopCLI.getSop().mergeCerts(), "merge-certs") @@ -45,4 +43,4 @@ class MergeCertsCmd : AbstractSopCmd() { throw RuntimeException(e) } } -} \ No newline at end of file +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt index 2afa015..08f9297 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt @@ -4,11 +4,11 @@ package sop.cli.picocli.commands +import java.io.IOException import picocli.CommandLine.Command import picocli.CommandLine.Option import sop.cli.picocli.SopCLI import sop.exception.SOPGPException.* -import java.io.IOException @Command( name = "update-key", @@ -25,8 +25,7 @@ class UpdateKeyCmd : AbstractSopCmd() { @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") var withKeyPassword: List = listOf() - @Option(names = ["--merge-certs"], paramLabel = "CERTS") - var mergeCerts: List = listOf() + @Option(names = ["--merge-certs"], paramLabel = "CERTS") var mergeCerts: List = listOf() override fun run() { val updateKey = throwIfUnsupportedSubcommand(SopCLI.getSop().updateKey(), "update-key") @@ -48,7 +47,8 @@ class UpdateKeyCmd : AbstractSopCmd() { val password = stringFromInputStream(getInput(passwordFileName)) updateKey.withKeyPassword(password) } catch (unsupportedOption: UnsupportedOption) { - val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") throw UnsupportedOption(errorMsg, unsupportedOption) } catch (e: IOException) { throw RuntimeException(e) @@ -57,7 +57,7 @@ class UpdateKeyCmd : AbstractSopCmd() { for (certInput in mergeCerts) { try { - getInput(certInput).use { certIn -> updateKey.mergeCerts(certIn) } + getInput(certInput).use { updateKey.mergeCerts(it) } } catch (e: IOException) { throw RuntimeException(e) } catch (badData: BadData) { @@ -71,6 +71,9 @@ class UpdateKeyCmd : AbstractSopCmd() { ready.writeTo(System.out) } catch (e: IOException) { throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", "STDIN") + throw BadData(errorMsg, badData) } } -} \ No newline at end of file +} From 1945411712d567acf7fa69b40fd71b78a7df1777 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 16:56:26 +0200 Subject: [PATCH 036/110] Update msg files with input/output information --- .../src/main/resources/msg_armor.properties | 8 ++++++-- .../src/main/resources/msg_armor_de.properties | 6 ++++-- .../src/main/resources/msg_certify-userid.properties | 6 +++--- .../main/resources/msg_certify-userid_de.properties | 6 +++--- .../main/resources/msg_change-key-password.properties | 9 +++++++-- .../resources/msg_change-key-password_de.properties | 5 ++++- .../src/main/resources/msg_dearmor.properties | 9 +++++++-- .../src/main/resources/msg_dearmor_de.properties | 5 ++++- .../src/main/resources/msg_decrypt.properties | 9 +++++++-- .../src/main/resources/msg_decrypt_de.properties | 5 ++++- .../src/main/resources/msg_detached-sign.properties | 9 +++++++-- .../main/resources/msg_detached-sign_de.properties | 5 ++++- .../src/main/resources/msg_detached-verify.properties | 9 +++++++-- .../main/resources/msg_detached-verify_de.properties | 5 ++++- .../src/main/resources/msg_encrypt.properties | 9 +++++++-- .../src/main/resources/msg_encrypt_de.properties | 5 ++++- .../src/main/resources/msg_extract-cert.properties | 9 +++++++-- .../src/main/resources/msg_extract-cert_de.properties | 5 ++++- .../src/main/resources/msg_generate-key.properties | 7 +++++-- .../src/main/resources/msg_generate-key_de.properties | 4 +++- .../src/main/resources/msg_help.properties | 4 ++-- .../src/main/resources/msg_help_de.properties | 2 +- .../src/main/resources/msg_inline-detach.properties | 9 +++++++-- .../main/resources/msg_inline-detach_de.properties | 5 ++++- .../src/main/resources/msg_inline-sign.properties | 9 +++++++-- .../src/main/resources/msg_inline-sign_de.properties | 5 ++++- .../src/main/resources/msg_inline-verify.properties | 9 +++++++-- .../main/resources/msg_inline-verify_de.properties | 5 ++++- .../src/main/resources/msg_list-profiles.properties | 7 +++++-- .../main/resources/msg_list-profiles_de.properties | 4 +++- .../src/main/resources/msg_merge-certs.properties | 10 ++++++++-- .../src/main/resources/msg_merge-certs_de.properties | 10 +++++----- .../src/main/resources/msg_revoke-key.properties | 11 ++++++++--- .../src/main/resources/msg_revoke-key_de.properties | 5 ++++- .../src/main/resources/msg_sop.properties | 1 + .../src/main/resources/msg_sop_de.properties | 1 + .../src/main/resources/msg_update-key.properties | 9 +++++++-- .../src/main/resources/msg_update-key_de.properties | 5 ++++- .../src/main/resources/msg_version.properties | 6 ++++-- .../src/main/resources/msg_version_de.properties | 4 +++- 40 files changed, 190 insertions(+), 66 deletions(-) diff --git a/sop-java-picocli/src/main/resources/msg_armor.properties b/sop-java-picocli/src/main/resources/msg_armor.properties index b4dcb59..1b7c1fb 100644 --- a/sop-java-picocli/src/main/resources/msg_armor.properties +++ b/sop-java-picocli/src/main/resources/msg_armor.properties @@ -3,9 +3,13 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Add ASCII Armor to standard input +standardInput=BINARY +standardInputDescription=OpenPGP material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED) +standardOutput=ARMORED +standardOutputDescription=Same material, but with ASCII-armoring added, if not already present + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_armor_de.properties b/sop-java-picocli/src/main/resources/msg_armor_de.properties index 4c365a8..34383c8 100644 --- a/sop-java-picocli/src/main/resources/msg_armor_de.properties +++ b/sop-java-picocli/src/main/resources/msg_armor_de.properties @@ -3,9 +3,11 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Schütze Standard-Eingabe mit ASCII Armor +standardInputDescription=OpenPGP Material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED) +standardOutputDescription=Dasselbe Material, aber mit ASCII Armor kodiert, falls noch nicht geschehen + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 -usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid.properties b/sop-java-picocli/src/main/resources/msg_certify-userid.properties index 5eb7aa3..252aae4 100644 --- a/sop-java-picocli/src/main/resources/msg_certify-userid.properties +++ b/sop-java-picocli/src/main/resources/msg_certify-userid.properties @@ -16,8 +16,8 @@ standardOutputDescription=Certified certificates stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 -usage.parameterListHeading=Parameters:%n +usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = Commands:%n -usage.optionListHeading = Options:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties index 0237fa6..9f0a673 100644 --- a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties +++ b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties @@ -13,8 +13,8 @@ standardInputDescription=Zertifikate, auf denen Identit standardOutputDescription=Zertifizierte Zertifikate # Generic TODO: Remove when bumping picocli to 4.7.0 -usage.parameterListHeading=Parameter:%n +usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 -usage.commandListHeading=Befehle:%n -usage.optionListHeading = Optionen:%n +usage.commandListHeading=%nBefehle:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_change-key-password.properties b/sop-java-picocli/src/main/resources/msg_change-key-password.properties index 3de3608..79bc11b 100644 --- a/sop-java-picocli/src/main/resources/msg_change-key-password.properties +++ b/sop-java-picocli/src/main/resources/msg_change-key-password.properties @@ -12,10 +12,15 @@ old-key-password.0=Old passwords to unlock the keys with. old-key-password.1=Multiple passwords can be passed in, which are tested sequentially to unlock locked subkeys. old-key-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +standardInput=KEYS +standardInputDescription=OpenPGP keys whose passphrases shall be changed +standardOutput=KEYS +standardOutputDescription=OpenPGP keys with changed passphrases + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nDescription:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties b/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties index 014c3e7..5515c1d 100644 --- a/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties +++ b/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties @@ -12,10 +12,13 @@ old-key-password.0=Alte Passw old-key-password.1=Mehrere Passwortkandidaten können übergeben werden, welche der Reihe nach durchprobiert werden, um Unterschlüssel zu entsperren. old-key-password.2=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +standardInputDescription=OpenPGP Schlüssel deren Passwörter geändert werden sollen +standardOutputDescription=OpenPGP Schlüssel mit geänderten Passwörtern + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nBeschreibung:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_dearmor.properties b/sop-java-picocli/src/main/resources/msg_dearmor.properties index b088de1..55cbf45 100644 --- a/sop-java-picocli/src/main/resources/msg_dearmor.properties +++ b/sop-java-picocli/src/main/resources/msg_dearmor.properties @@ -3,9 +3,14 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Remove ASCII Armor from standard input +standardInput=ARMORED +standardInputDescription=Armored OpenPGP material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED) +standardOutput=BINARY +standardOutputDescription=Same material, but with ASCII-armoring removed + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_dearmor_de.properties b/sop-java-picocli/src/main/resources/msg_dearmor_de.properties index 362ccef..e01ab7a 100644 --- a/sop-java-picocli/src/main/resources/msg_dearmor_de.properties +++ b/sop-java-picocli/src/main/resources/msg_dearmor_de.properties @@ -3,9 +3,12 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Entferne ASCII Armor von Standard-Eingabe +standardInputDescription=OpenPGP Material mit ASCII Armor (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED) +standardOutputDescription=Dasselbe Material, aber mit entfernter ASCII Armor + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_decrypt.properties b/sop-java-picocli/src/main/resources/msg_decrypt.properties index 5903ded..bec315f 100644 --- a/sop-java-picocli/src/main/resources/msg_decrypt.properties +++ b/sop-java-picocli/src/main/resources/msg_decrypt.properties @@ -22,10 +22,15 @@ with-key-password.0=Passphrase to unlock the secret key(s). with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). KEY[0..*]=Secret keys to attempt decryption with +standardInput=CIPHERTEXT +standardInputDescription=Encrypted OpenPGP message +standardOutput=DATA +standardOutputDescription=Decrypted OpenPGP message + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_decrypt_de.properties b/sop-java-picocli/src/main/resources/msg_decrypt_de.properties index ba40897..395a89f 100644 --- a/sop-java-picocli/src/main/resources/msg_decrypt_de.properties +++ b/sop-java-picocli/src/main/resources/msg_decrypt_de.properties @@ -22,10 +22,13 @@ with-key-password.0=Passwort zum Entsperren der privaten Schl with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). KEY[0..*]=Private Schlüssel zum Entschlüsseln der Nachricht +standardInputDescription=Verschlüsselte OpenPGP Nachricht +standardOutputDescription=Entschlüsselte OpenPGP Nachricht + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_detached-sign.properties b/sop-java-picocli/src/main/resources/msg_detached-sign.properties index 83359a6..6ebfd0b 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-sign.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-sign.properties @@ -11,10 +11,15 @@ with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, f 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). KEYS[0..*]=Secret keys used for signing +standardInput=DATA +standardInputDescription=Data that shall be signed +standardOutput=SIGNATURES +standardOutputDescription=Detached OpenPGP signature(s) + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_detached-sign_de.properties b/sop-java-picocli/src/main/resources/msg_detached-sign_de.properties index b943da5..39b59b5 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-sign_de.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-sign_de.properties @@ -11,10 +11,13 @@ with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, 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. KEYS[0..*]=Private Signaturschlüssel +standardInputDescription=Daten die signiert werden sollen +standardOutputDescription=Abgetrennte OpenPGP Signatur(en) + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_detached-verify.properties b/sop-java-picocli/src/main/resources/msg_detached-verify.properties index ee1a468..074a318 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-verify.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-verify.properties @@ -13,11 +13,16 @@ not-after.3=Accepts special value "-" for end of time. SIGNATURE[0]=Detached signature CERT[1..*]=Public key certificates for signature verification +standardInput=DATA +standardInputDescription=Data over which the detached signatures were calculated +standardOutput=VERIFICATIONS +standardOutputDescription=Information about successfully verified signatures + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nDescription:%n usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_detached-verify_de.properties b/sop-java-picocli/src/main/resources/msg_detached-verify_de.properties index 332bff6..e21ee2a 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-verify_de.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-verify_de.properties @@ -13,11 +13,14 @@ not-after.3=Akzeptiert speziellen Wert '-' f SIGNATURE[0]=Abgetrennte Signatur CERT[1..*]=Zertifikate (öffentliche Schlüssel) zur Signaturprüfung +standardInputDescription=Daten, über die die abgetrennten Signaturen erstellt wurden +standardOutputDescription=Informationen über erfolgreich verifizierte Signaturen + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nBeschreibung:%n usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_encrypt.properties b/sop-java-picocli/src/main/resources/msg_encrypt.properties index c0f7f7d..7bbf593 100644 --- a/sop-java-picocli/src/main/resources/msg_encrypt.properties +++ b/sop-java-picocli/src/main/resources/msg_encrypt.properties @@ -12,10 +12,15 @@ with-key-password.0=Passphrase to unlock the secret key(s). with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). CERTS[0..*]=Certificates the message gets encrypted to +standardInput=DATA +standardInputDescription=Data that shall be encrypted +standardOutput=CIPHERTEXT +standardOutputDescription=Encrypted OpenPGP message + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_encrypt_de.properties b/sop-java-picocli/src/main/resources/msg_encrypt_de.properties index 6a3055c..55b0338 100644 --- a/sop-java-picocli/src/main/resources/msg_encrypt_de.properties +++ b/sop-java-picocli/src/main/resources/msg_encrypt_de.properties @@ -12,10 +12,13 @@ with-key-password.0=Passwort zum Entsperren der privaten Schl with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). CERTS[0..*]=Zertifikate für die die Nachricht verschlüsselt werden soll +standardInputDescription=Daten, die verschlüsselt werden sollen +standardOutputDescription=Verschlüsselte OpenPGP Nachricht + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_extract-cert.properties b/sop-java-picocli/src/main/resources/msg_extract-cert.properties index 82cac0f..1d1dee4 100644 --- a/sop-java-picocli/src/main/resources/msg_extract-cert.properties +++ b/sop-java-picocli/src/main/resources/msg_extract-cert.properties @@ -5,10 +5,15 @@ usage.header=Extract a public key certificate from a secret key usage.description=Read a secret key from STDIN and emit the public key certificate to STDOUT. no-armor=ASCII armor the output +standardInput=KEYS +standardInputDescription=Private key(s), from which certificate(s) shall be extracted +standardOutput=CERTS +standardOutputDescription=Extracted certificate(s) + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nDescription:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_extract-cert_de.properties b/sop-java-picocli/src/main/resources/msg_extract-cert_de.properties index 0946cfc..c92d31d 100644 --- a/sop-java-picocli/src/main/resources/msg_extract-cert_de.properties +++ b/sop-java-picocli/src/main/resources/msg_extract-cert_de.properties @@ -5,10 +5,13 @@ usage.header=Extrahiere Zertifikat ( usage.description=Lese einen Schlüssel von Standard-Eingabe und gebe das Zertifikat auf Standard-Ausgabe aus. no-armor=Schütze Ausgabe mit ASCII Armor +standardInputDescription=Private Schlüssel, deren Zertifikate extrahiert werden sollen +standardOutputDescription=Extrahierte Zertifikate + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nBeschreibung:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_generate-key.properties b/sop-java-picocli/src/main/resources/msg_generate-key.properties index 60ff4a4..c17f7f6 100644 --- a/sop-java-picocli/src/main/resources/msg_generate-key.properties +++ b/sop-java-picocli/src/main/resources/msg_generate-key.properties @@ -9,10 +9,13 @@ signing-only=Generate a key that can only be used for signing with-key-password.0=Password to protect the private key with with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +standardOutput=KEYS +standardOutputDescription=Generated OpenPGP key + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_generate-key_de.properties b/sop-java-picocli/src/main/resources/msg_generate-key_de.properties index 6a0ce13..84db04d 100644 --- a/sop-java-picocli/src/main/resources/msg_generate-key_de.properties +++ b/sop-java-picocli/src/main/resources/msg_generate-key_de.properties @@ -9,10 +9,12 @@ signing-only=Generiere einen Schl with-key-password.0=Passwort zum Schutz des privaten Schlüssels with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +standardOutputDescription=Erzeugter OpenPGP Schlüssel + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_help.properties b/sop-java-picocli/src/main/resources/msg_help.properties index 797cc79..637c1d0 100644 --- a/sop-java-picocli/src/main/resources/msg_help.properties +++ b/sop-java-picocli/src/main/resources/msg_help.properties @@ -6,6 +6,6 @@ usage.header=Display usage information for the specified subcommand stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_help_de.properties b/sop-java-picocli/src/main/resources/msg_help_de.properties index beea45c..8471188 100644 --- a/sop-java-picocli/src/main/resources/msg_help_de.properties +++ b/sop-java-picocli/src/main/resources/msg_help_de.properties @@ -7,5 +7,5 @@ stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-detach.properties b/sop-java-picocli/src/main/resources/msg_inline-detach.properties index c100c51..ca0ed6b 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-detach.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-detach.properties @@ -5,9 +5,14 @@ usage.header=Split signatures from a clearsigned message no-armor=ASCII armor the output signatures-out=Destination to which a detached signatures block will be written +standardInput=INLINESIGNED +standardInputDescription=Inline-signed OpenPGP message +standardOutput=DATA +standardOutputDescription=The message without any signatures + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-detach_de.properties b/sop-java-picocli/src/main/resources/msg_inline-detach_de.properties index e59aa34..84b8c47 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-detach_de.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-detach_de.properties @@ -5,9 +5,12 @@ usage.header=Trenne Signaturen von Klartext-signierter Nachricht no-armor=Schütze Ausgabe mit ASCII Armor signatures-out=Schreibe abgetrennte Signaturen in Ausgabe +standardInputDescription=Klartext-signierte OpenPGP Nachricht +standardOutputDescription=Nachricht ohne Signaturen + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-sign.properties b/sop-java-picocli/src/main/resources/msg_inline-sign.properties index f5143bb..936b417 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-sign.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-sign.properties @@ -13,10 +13,15 @@ with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, f 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). KEYS[0..*]=Secret keys used for signing +standardInput=DATA +standardInputDescription=Data that shall be signed +standardOutput=INLINESIGNED +standardOutputDescription=Inline-signed OpenPGP message + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-sign_de.properties b/sop-java-picocli/src/main/resources/msg_inline-sign_de.properties index b09b7e4..f8fe906 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-sign_de.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-sign_de.properties @@ -13,10 +13,13 @@ with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, 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. KEYS[0..*]=Private Signaturschlüssel +standardInputDescription=Daten, die signiert werden sollen +standardOutputDescription=Inline-signierte OpenPGP Nachricht + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-verify.properties b/sop-java-picocli/src/main/resources/msg_inline-verify.properties index dfa94d7..2e0d69f 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-verify.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-verify.properties @@ -12,10 +12,15 @@ not-after.3=Accepts special value "-" for end of time. verifications-out=File to write details over successful verifications to CERT[0..*]=Public key certificates for signature verification +standardInput=INLINESIGNED +standardInputDescription=Inline-signed OpenPGP message +standardOutput=DATA +standardOutputDescription=The message without any signatures + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-verify_de.properties b/sop-java-picocli/src/main/resources/msg_inline-verify_de.properties index a9a5722..9b70504 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-verify_de.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-verify_de.properties @@ -12,10 +12,13 @@ not-after.3=Akzeptiert speziellen Wert '-' f verifications-out=Schreibe Status der Signaturprüfung in angegebene Ausgabe CERT[0..*]=Zertifikate (öffentlich Schlüssel) zur Signaturprüfung +standardInputDescription=Inline-signierte OpenPGP Nachricht +standardOutputDescription=Nachricht ohne Signaturen + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_list-profiles.properties b/sop-java-picocli/src/main/resources/msg_list-profiles.properties index 6d5f1a8..3defe8e 100644 --- a/sop-java-picocli/src/main/resources/msg_list-profiles.properties +++ b/sop-java-picocli/src/main/resources/msg_list-profiles.properties @@ -4,10 +4,13 @@ usage.header=Emit a list of profiles supported by the identified subcommand subcommand=Subcommand for which to list profiles +standardOutput=PROFILELIST +standardOutputDescription=List of profiles supported by the identified subcommand + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_list-profiles_de.properties b/sop-java-picocli/src/main/resources/msg_list-profiles_de.properties index ac03c0d..093aeb3 100644 --- a/sop-java-picocli/src/main/resources/msg_list-profiles_de.properties +++ b/sop-java-picocli/src/main/resources/msg_list-profiles_de.properties @@ -4,10 +4,12 @@ usage.header=Gebe eine Liste von Profilen aus, welche vom angegebenen Unterbefehl unterstützt werden subcommand=Unterbefehl, für welchen Profile gelistet werden sollen +standardOutputDescription=Liste von Profilen, die der identifizierte Unterbefehl unterstützt + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_merge-certs.properties b/sop-java-picocli/src/main/resources/msg_merge-certs.properties index 866db4b..b01f577 100644 --- a/sop-java-picocli/src/main/resources/msg_merge-certs.properties +++ b/sop-java-picocli/src/main/resources/msg_merge-certs.properties @@ -6,10 +6,16 @@ usage.description=BLABLA no-armor=ASCII armor the output CERTS[0..*]=OpenPGP certificates from which updates shall be merged into the base certificates from standard input +standardInput=CERTS +standardInputDescription=Base certificates into which additional elements from the command line shall be merged +standardOutput=CERTS +standardOutputDescription=Merged certificates + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.descriptionHeading=%nNote:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties b/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties index 021c535..b1f008c 100644 --- a/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties +++ b/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties @@ -1,19 +1,19 @@ # SPDX-FileCopyrightText: 2024 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.headerHeading=OpenPGP Zertifikate zusammenführen%n%n +usage.headerHeading=OpenPGP Zertifikate zusammenführen%n usage.header=Führe OpenPGP Zertifikate aus der Standardeingabe mit ensprechenden Elementen aus CERTS zusammen und gebe das Ergebnis auf der Standardausgabe aus usage.description=Es werden nur Zertifikate auf die Standardausgabe geschrieben, welche Teil der Standardeingabe waren no-armor=Schütze Ausgabe mit ASCII Armor CERTS[0..*]=OpenPGP Zertifikate aus denen neue Elemente in die Basiszertifikate aus der Standardeingabe übernommen werden sollen -usage.parameterList.0=STANDARDIN -usage.parameterList.1=STANDARDOUT +standardInputDescription=Basis-Zertifikate, in welche zusätzliche Elemente von der Kommandozeile zusammengeführt werden sollen +standardOutputDescription=Zusammengeführte Zertifikate # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 -usage.descriptionHeading=%nHinweise:%n +usage.descriptionHeading=%nHinweis:%n usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_revoke-key.properties b/sop-java-picocli/src/main/resources/msg_revoke-key.properties index c7d72b3..f68b774 100644 --- a/sop-java-picocli/src/main/resources/msg_revoke-key.properties +++ b/sop-java-picocli/src/main/resources/msg_revoke-key.properties @@ -7,10 +7,15 @@ no-armor=ASCII armor the output with-key-password.0=Passphrase to unlock the secret key(s). with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +standardInput=KEYS +standardInputDescription=OpenPGP key that shall be revoked +standardOutput=CERTS +standardOutputDescription=Revocation certificate + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 -usage.descriptionHeading=%nDescription:%n +usage.descriptionHeading=D%nescription:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties b/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties index 95db272..fa8c5b4 100644 --- a/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties +++ b/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties @@ -7,10 +7,13 @@ no-armor=Sch with-key-password.0=Passwort zum Entsperren der privaten Schlüssel with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +standardInputDescription=OpenPGP Schlüssel, der widerrufen werden soll +standardOutputDescription=Widerrufszertifikat + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nBeschreibung:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index d5d997a..097a2e2 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -11,6 +11,7 @@ locale=Locale for description texts usage.synopsisHeading=Usage:\u0020 usage.commandListHeading=%nCommands:%n usage.optionListHeading=%nOptions:%n +usage.parameterListHeading=%nParameters:%n usage.footerHeading=Powered by picocli%n standardInputHeading=%nInput:%n diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 73efe89..99d28a7 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -11,6 +11,7 @@ locale=Gebietsschema f usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n usage.optionListHeading=%nOptionen:%n +usage.parameterListHeading=%nParameter:%n usage.footerHeading=Powered by Picocli%n standardInputHeading=%nEingabe:%n diff --git a/sop-java-picocli/src/main/resources/msg_update-key.properties b/sop-java-picocli/src/main/resources/msg_update-key.properties index dd4446d..e12fbbc 100644 --- a/sop-java-picocli/src/main/resources/msg_update-key.properties +++ b/sop-java-picocli/src/main/resources/msg_update-key.properties @@ -10,10 +10,15 @@ with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, f merge-certs.0=Merge additional elements found in the corresponding CERTS objects into the updated secret keys merge-certs.1=This can be used, for example, to absorb a third-party certification into the Transferable Secret Key +standardInput=KEYS +standardInputDescription=OpenPGP key that shall be kept up-to-date +standardOutput=KEYS +standardOutputDescription=Updated OpenPGP key + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_update-key_de.properties b/sop-java-picocli/src/main/resources/msg_update-key_de.properties index 86b999e..1b8a84d 100644 --- a/sop-java-picocli/src/main/resources/msg_update-key_de.properties +++ b/sop-java-picocli/src/main/resources/msg_update-key_de.properties @@ -10,9 +10,12 @@ with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dat merge-certs.0=Führe zusätzliche Elemente aus entsprechenden CERTS Objekten mit dem privaten Schlüssel zusammen merge-certs.1=Dies kann zum Beispiel dazu genutzt werden, Zertifizierungen dritter in den privaten Schlüssel zu übernehmen +standardInputDescription=OpenPGP Schlüssel, der auf den neusten Stand gebracht werden soll +standardOutputDescription=Erneuerter OpenPGP Schlüssel + # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_version.properties b/sop-java-picocli/src/main/resources/msg_version.properties index 9e1451b..c7d0168 100644 --- a/sop-java-picocli/src/main/resources/msg_version.properties +++ b/sop-java-picocli/src/main/resources/msg_version.properties @@ -6,9 +6,11 @@ extended=Print an extended version string backend=Print information about the cryptographic backend sop-spec=Print the latest revision of the SOP specification targeted by the implementation +standardOutput=version information + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_version_de.properties b/sop-java-picocli/src/main/resources/msg_version_de.properties index 608b0c6..c317916 100644 --- a/sop-java-picocli/src/main/resources/msg_version_de.properties +++ b/sop-java-picocli/src/main/resources/msg_version_de.properties @@ -6,9 +6,11 @@ extended=Gebe erweiterte Versionsinformationen aus backend=Gebe Informationen über das kryptografische Backend aus sop-spec=Gebe die neuste Revision der SOP Spezifikation aus, welche von dieser Implementierung umgesetzt wird +standardOutput=Versionsinformationen + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n From 0ec2961cbe523f1e1b11791e4d2684da761ae6b4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 16:56:25 +0200 Subject: [PATCH 037/110] Implement validate-userid command --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 1 + .../cli/picocli/commands/ValidateUserIdCmd.kt | 74 ++++++++++++++++++ .../src/main/resources/msg_sop.properties | 2 + .../resources/msg_validate-userid.properties | 18 +++++ .../msg_validate-userid_de.properties | 18 +++++ sop-java/src/main/kotlin/sop/SOP.kt | 15 ++-- .../kotlin/sop/exception/SOPGPException.kt | 27 +++++-- .../kotlin/sop/operation/ValidateUserId.kt | 78 +++++++++++++++++++ 8 files changed, 216 insertions(+), 17 deletions(-) create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt create mode 100644 sop-java-picocli/src/main/resources/msg_validate-userid.properties create mode 100644 sop-java-picocli/src/main/resources/msg_validate-userid_de.properties create mode 100644 sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index ca62038..6caf2ff 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -30,6 +30,7 @@ import sop.exception.SOPGPException UpdateKeyCmd::class, MergeCertsCmd::class, CertifyUserIdCmd::class, + ValidateUserIdCmd::class, // Messaging subcommands SignCmd::class, VerifyCmd::class, diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt new file mode 100644 index 0000000..c2de148 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import picocli.CommandLine.Parameters +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.util.HexUtil.Companion.bytesToHex + +@Command( + name = "validate-userid", + resourceBundle = "msg_validate-userid", + exitCodeOnInvalidInput = SOPGPException.MissingArg.EXIT_CODE) +class ValidateUserIdCmd : AbstractSopCmd() { + + @Option(names = ["--addr-spec-only"]) var addrSpecOnly: Boolean = false + + @Parameters(index = "0", arity = "1", paramLabel = "USERID") lateinit var userId: String + + @Parameters(index = "1..*", arity = "1..*", paramLabel = "CERTS") + var authorities: List = listOf() + + override fun run() { + val validateUserId = + throwIfUnsupportedSubcommand(SopCLI.getSop().validateUserId(), "validate-userid") + + if (addrSpecOnly) { + validateUserId.addrSpecOnly() + } + + validateUserId.userId(userId) + + for (authority in authorities) { + try { + getInput(authority).use { validateUserId.authorities(it) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (b: SOPGPException.BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", authority) + throw SOPGPException.BadData(errorMsg, b) + } + } + + try { + val valid = validateUserId.subjects(System.`in`) + + if (!valid) { + val errorMsg = getMsg("sop.error.runtime.any_cert_user_id_no_match", userId) + throw SOPGPException.CertUserIdNoMatch(errorMsg) + } + } catch (e: SOPGPException.CertUserIdNoMatch) { + val errorMsg = + if (e.fingerprint != null) { + getMsg( + "sop.error.runtime.cert_user_id_no_match", + bytesToHex(e.fingerprint!!), + userId) + } else { + getMsg("sop.error.runtime.any_cert_user_id_no_match", userId) + } + throw SOPGPException.CertUserIdNoMatch(errorMsg, e) + } catch (e: SOPGPException.BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", "STDIN") + throw SOPGPException.BadData(errorMsg, e) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 097a2e2..520533a 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -80,6 +80,8 @@ sop.error.runtime.cert_cannot_encrypt=Certificate from input '%s' 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. sop.error.runtime.cannot_decrypt_message=Message could not be decrypted. +sop.error.runtime.cert_user_id_no_match=Certificate '%s' does not contain a valid binding for user id '%s'. +sop.error.runtime.any_cert_user_id_no_match=Any certificate does not contain a valid binding for user id '%s'. ## 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 '%s' is required. diff --git a/sop-java-picocli/src/main/resources/msg_validate-userid.properties b/sop-java-picocli/src/main/resources/msg_validate-userid.properties new file mode 100644 index 0000000..5cfed2d --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_validate-userid.properties @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Validate a UserID in an OpenPGP certificate +addr-spec-only=Treat the USERID as an email address, match only against the email address part of each correctly bound UserID +USERID[0]=UserID +CERTS[1..*]=Authority OpenPGP certificates + +standardInput=CERTS +standardInputDescription=OpenPGP certificates in which UserID bindings shall be validated + +stacktrace=Print stacktrace +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=%nParameters:%n +usage.synopsisHeading=Usage:\u0020 +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n +usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties b/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties new file mode 100644 index 0000000..8231c6a --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Validiere eine UserID auf OpenPGP Zertifikaten +addr-spec-only=Behandle die USERID als E-Mail-Adresse, vergleiche sie nur mit dem E-Mail-Adressen-Teil jeder korrekten UserID +USERID[0]=UserID +CERTS[1..*]=Autoritäre OpenPGP Zertifikate + +standardInput=CERTS +standardInputDescription=OpenPGP Zertifikate auf denen UserIDs validiert werden sollen + +stacktrace=Print stacktrace +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=%nParameter:%n +usage.synopsisHeading=Aufruf:\u0020 +usage.commandListHeading=%nBefehle:%n +usage.optionListHeading=%nOptionen:%n +usage.footerHeading=Powered by Picocli%n diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index 5435cad..c5f05e2 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -60,18 +60,15 @@ interface SOP : SOPV { /** Update a key's password. */ fun changeKeyPassword(): ChangeKeyPassword - /** - * Keep a secret key up-to-date. - */ + /** Keep a secret key up-to-date. */ fun updateKey(): UpdateKey - /** - * Merge OpenPGP certificates. - */ + /** Merge OpenPGP certificates. */ fun mergeCerts(): MergeCerts - /** - * Certify OpenPGP Certificate User-IDs. - */ + /** Certify OpenPGP Certificate User-IDs. */ fun certifyUserId(): CertifyUserId + + /** Validate a UserID in an OpenPGP certificate. */ + fun validateUserId(): ValidateUserId } diff --git a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt index 1f9ce6b..862e1bd 100644 --- a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt +++ b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt @@ -338,9 +338,7 @@ abstract class SOPGPException : RuntimeException { } } - /** - * The primary key of a KEYS object is too weak or revoked. - */ + /** The primary key of a KEYS object is too weak or revoked. */ class PrimaryKeyBad : SOPGPException { constructor() : super() @@ -353,13 +351,26 @@ abstract class SOPGPException : RuntimeException { } } - /** - * The CERTS object has no matching User ID. - */ + /** The CERTS object has no matching User ID. */ class CertUserIdNoMatch : SOPGPException { - constructor() : super() - constructor(errorMsg: String) : super(errorMsg) + val fingerprint: ByteArray? + + constructor() : super() { + fingerprint = null + } + + constructor(fingerprint: ByteArray) : super() { + this.fingerprint = fingerprint + } + + constructor(errorMsg: String) : super(errorMsg) { + fingerprint = null + } + + constructor(errorMsg: String, cause: Throwable) : super(errorMsg, cause) { + fingerprint = null + } override fun getExitCode(): Int = EXIT_CODE diff --git a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt new file mode 100644 index 0000000..4f4c51a --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.exception.SOPGPException + +/** Subcommand to validate UserIDs on certificates. */ +interface ValidateUserId { + + /** + * If this is set, then the USERID is treated as an e-mail address, and matched only against the + * e-mail address part of each correctly bound User ID. The rest of each correctly bound User ID + * is ignored. + * + * @return this + */ + @Throws(SOPGPException.UnsupportedOption::class) fun addrSpecOnly(): ValidateUserId + + /** + * Set the UserID to validate. To match only the email address, call [addrSpecOnly]. + * + * @param userId UserID or email address + * @return this + */ + fun userId(userId: String): ValidateUserId + + /** + * Add certificates, which act as authorities. The [userId] is only considered correctly bound, + * if it was bound by an authoritative certificate. + * + * @param certs authoritative certificates + * @return this + */ + @Throws(SOPGPException.BadData::class, IOException::class) + fun authorities(certs: InputStream): ValidateUserId + + /** + * Add certificates, which act as authorities. The [userId] is only considered correctly bound, + * if it was bound by an authoritative certificate. + * + * @param certs authoritative certificates + * @return this + */ + @Throws(SOPGPException.BadData::class, IOException::class) + fun authorities(certs: ByteArray): ValidateUserId = authorities(certs.inputStream()) + + /** + * Add subject certificates, on which UserID bindings are validated. + * + * @param certs subject certificates + * @return true if all subject certificates have a correct binding to the UserID. + * @throws SOPGPException.BadData if the subject certificates are malformed + * @throws IOException if a parser exception happens + * @throws SOPGPException.CertUserIdNoMatch if any subject certificate does not have a correctly + * bound UserID that matches [userId]. + */ + @Throws( + SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + fun subjects(certs: InputStream): Boolean + + /** + * Add subject certificates, on which UserID bindings are validated. + * + * @param certs subject certificates + * @return true if all subject certificates have a correct binding to the UserID. + * @throws SOPGPException.BadData if the subject certificates are malformed + * @throws IOException if a parser exception happens + * @throws SOPGPException.CertUserIdNoMatch if any subject certificate does not have a correctly + * bound UserID that matches [userId]. + */ + @Throws( + SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + fun subjects(certs: ByteArray): Boolean = subjects(certs.inputStream()) +} From 2ec7088c120eb2e8bbc858bd6785fc3768db49de Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 16:56:43 +0200 Subject: [PATCH 038/110] Checkstyle --- .../kotlin/sop/operation/CertifyUserId.kt | 24 ++++++++--------- .../main/kotlin/sop/operation/MergeCerts.kt | 9 +++---- .../main/kotlin/sop/operation/UpdateKey.kt | 27 +++++++++++++------ 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt index 92fff20..642966b 100644 --- a/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt +++ b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt @@ -4,28 +4,26 @@ package sop.operation +import java.io.IOException +import java.io.InputStream import sop.Ready import sop.exception.SOPGPException import sop.util.UTF8Util -import java.io.IOException -import java.io.InputStream interface CertifyUserId { - @Throws(SOPGPException.UnsupportedOption::class) - fun noArmor(): CertifyUserId + @Throws(SOPGPException.UnsupportedOption::class) fun noArmor(): CertifyUserId - @Throws(SOPGPException.UnsupportedOption::class) - fun userId(userId: String): CertifyUserId + @Throws(SOPGPException.UnsupportedOption::class) fun userId(userId: String): CertifyUserId @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) - fun withKeyPassword(password: String): CertifyUserId = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + fun withKeyPassword(password: String): CertifyUserId = + withKeyPassword(password.toByteArray(UTF8Util.UTF8)) @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) fun withKeyPassword(password: ByteArray): CertifyUserId - @Throws(SOPGPException.UnsupportedOption::class) - fun noRequireSelfSig(): CertifyUserId + @Throws(SOPGPException.UnsupportedOption::class) fun noRequireSelfSig(): CertifyUserId @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class) fun keys(keys: InputStream): CertifyUserId @@ -33,9 +31,11 @@ interface CertifyUserId { @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class) fun keys(keys: ByteArray): CertifyUserId = keys(keys.inputStream()) - @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + @Throws( + SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) fun certs(certs: InputStream): Ready - @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + @Throws( + SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) fun certs(certs: ByteArray): Ready = certs(certs.inputStream()) -} \ No newline at end of file +} diff --git a/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt index f60d291..f922490 100644 --- a/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt +++ b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt @@ -4,15 +4,14 @@ package sop.operation -import sop.Ready -import sop.exception.SOPGPException import java.io.IOException import java.io.InputStream +import sop.Ready +import sop.exception.SOPGPException interface MergeCerts { - @Throws(SOPGPException.UnsupportedOption::class) - fun noArmor(): MergeCerts + @Throws(SOPGPException.UnsupportedOption::class) fun noArmor(): MergeCerts @Throws(SOPGPException.BadData::class, IOException::class) fun updates(updateCerts: InputStream): MergeCerts @@ -25,4 +24,4 @@ interface MergeCerts { @Throws(SOPGPException.BadData::class, IOException::class) fun baseCertificates(certs: ByteArray): Ready = baseCertificates(certs.inputStream()) -} \ No newline at end of file +} diff --git a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt index 1b12f6f..6c32b22 100644 --- a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt @@ -4,11 +4,11 @@ package sop.operation +import java.io.IOException +import java.io.InputStream import sop.Ready import sop.exception.SOPGPException import sop.util.UTF8Util -import java.io.IOException -import java.io.InputStream interface UpdateKey { @@ -24,20 +24,31 @@ interface UpdateKey { @Throws(SOPGPException.UnsupportedOption::class) fun noNewMechanisms(): UpdateKey @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) - fun withKeyPassword(password: String): UpdateKey = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + fun withKeyPassword(password: String): UpdateKey = + withKeyPassword(password.toByteArray(UTF8Util.UTF8)) @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) fun withKeyPassword(password: ByteArray): UpdateKey - @Throws(SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) + @Throws( + SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) fun mergeCerts(certs: InputStream): UpdateKey - @Throws(SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) + @Throws( + SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) fun mergeCerts(certs: ByteArray): UpdateKey = mergeCerts(certs.inputStream()) - @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class, SOPGPException.PrimaryKeyBad::class) + @Throws( + SOPGPException.BadData::class, + IOException::class, + SOPGPException.KeyIsProtected::class, + SOPGPException.PrimaryKeyBad::class) fun key(key: InputStream): Ready - @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class, SOPGPException.PrimaryKeyBad::class) + @Throws( + SOPGPException.BadData::class, + IOException::class, + SOPGPException.KeyIsProtected::class, + SOPGPException.PrimaryKeyBad::class) fun key(key: ByteArray): Ready = key(key.inputStream()) -} \ No newline at end of file +} From 8c19f2f31db521a74a13b62b7bec389d29492e67 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 17:51:04 +0200 Subject: [PATCH 039/110] Implement external variants of new subcommands --- .../main/kotlin/sop/external/ExternalSOP.kt | 20 ++++++++ .../operation/CertifyUserIdExternal.kt | 48 +++++++++++++++++++ .../external/operation/MergeCertsExternal.kt | 30 ++++++++++++ .../external/operation/UpdateKeyExternal.kt | 41 ++++++++++++++++ .../operation/ValidateUserIdExternal.kt | 38 +++++++++++++++ 5 files changed, 177 insertions(+) create mode 100644 external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt create mode 100644 external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt create mode 100644 external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt create mode 100644 external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt index 27c93ae..8ab7737 100644 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -69,6 +69,14 @@ class ExternalSOP( override fun changeKeyPassword(): ChangeKeyPassword = ChangeKeyPasswordExternal(binaryName, properties) + override fun updateKey(): UpdateKey = UpdateKeyExternal(binaryName, properties) + + override fun mergeCerts(): MergeCerts = MergeCertsExternal(binaryName, properties) + + override fun certifyUserId(): CertifyUserId = CertifyUserIdExternal(binaryName, properties) + + override fun validateUserId(): ValidateUserId = ValidateUserIdExternal(binaryName, properties) + /** * This interface can be used to provide a directory in which external SOP binaries can * temporarily store additional results of OpenPGP operations such that the binding classes can @@ -169,6 +177,18 @@ class ExternalSOP( UnsupportedProfile.EXIT_CODE -> throw UnsupportedProfile( "External SOP backend reported error UnsupportedProfile ($exitCode):\n$errorMessage") + NoHardwareKeyFound.EXIT_CODE -> + throw NoHardwareKeyFound( + "External SOP backend reported error NoHardwareKeyFound ($exitCode):\n$errorMessage") + HardwareKeyFailure.EXIT_CODE -> + throw HardwareKeyFailure( + "External SOP backend reported error HardwareKeyFalure ($exitCode):\n$errorMessage") + PrimaryKeyBad.EXIT_CODE -> + throw PrimaryKeyBad( + "External SOP backend reported error PrimaryKeyBad ($exitCode):\n$errorMessage") + CertUserIdNoMatch.EXIT_CODE -> + throw CertUserIdNoMatch( + "External SOP backend reported error CertUserIdNoMatch ($exitCode):\n$errorMessage") // Did you forget to add a case for a new exception type? else -> diff --git a/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt new file mode 100644 index 0000000..abf4d50 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.* +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.CertifyUserId + +class CertifyUserIdExternal(binary: String, environment: Properties) : CertifyUserId { + + private val commandList = mutableListOf(binary, "version") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCount = 0 + + private val keys: MutableList = mutableListOf() + + override fun noArmor(): CertifyUserId = apply { commandList.add("--no-armor") } + + override fun userId(userId: String): CertifyUserId = apply { + commandList.add("--userid") + commandList.add(userId) + } + + override fun withKeyPassword(password: ByteArray): CertifyUserId = apply { + commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount") + envList.add("KEY_PASSWORD_$argCount=${String(password)}") + argCount += 1 + } + + override fun noRequireSelfSig(): CertifyUserId = apply { + commandList.add("--no-require-self-sig") + } + + override fun keys(keys: InputStream): CertifyUserId = apply { + this.keys.add("@ENV:KEY_$argCount") + envList.add("KEY_$argCount=${ExternalSOP.readString(keys)}") + argCount += 1 + } + + override fun certs(certs: InputStream): Ready = + ExternalSOP.executeTransformingOperation( + Runtime.getRuntime(), commandList.plus(keys), envList, certs) +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt new file mode 100644 index 0000000..0869fab --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.* +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.MergeCerts + +class MergeCertsExternal(binary: String, environment: Properties) : MergeCerts { + + private val commandList = mutableListOf(binary, "version") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCount = 0 + + override fun noArmor(): MergeCerts = apply { commandList.add("--no-armor") } + + override fun updates(updateCerts: InputStream): MergeCerts = apply { + commandList.add("@ENV:CERT_$argCount") + envList.add("CERT_$argCount=${ExternalSOP.readString(updateCerts)}") + argCount += 1 + } + + override fun baseCertificates(certs: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, certs) +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt new file mode 100644 index 0000000..9aa1d29 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.* +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.UpdateKey + +class UpdateKeyExternal(binary: String, environment: Properties) : UpdateKey { + + private val commandList = mutableListOf(binary, "update-key") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCount = 0 + + override fun noArmor(): UpdateKey = apply { commandList.add("--no-armor") } + + override fun signingOnly(): UpdateKey = apply { commandList.add("--signing-only") } + + override fun noNewMechanisms(): UpdateKey = apply { commandList.add("--no-new-mechanisms") } + + override fun withKeyPassword(password: ByteArray): UpdateKey = apply { + commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount") + envList.add("KEY_PASSWORD_$argCount=${String(password)}") + argCount += 1 + } + + override fun mergeCerts(certs: InputStream): UpdateKey = apply { + commandList.add("--merge-certs") + commandList.add("@ENV:CERT_$argCount") + envList.add("CERT_$argCount=${ExternalSOP.readString(certs)}") + argCount += 1 + } + + override fun key(key: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, key) +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt new file mode 100644 index 0000000..867a755 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.* +import sop.external.ExternalSOP +import sop.operation.ValidateUserId + +class ValidateUserIdExternal(binary: String, environment: Properties) : ValidateUserId { + + private val commandList = mutableListOf(binary, "version") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCount = 0 + + private var userId: String? = null + private val authorities: MutableList = mutableListOf() + + override fun addrSpecOnly(): ValidateUserId = apply { commandList.add("--addr-spec-only") } + + override fun userId(userId: String): ValidateUserId = apply { this.userId = userId } + + override fun authorities(certs: InputStream): ValidateUserId = apply { + this.authorities.add("@ENV:CERT_$argCount") + envList.add("CERT_$argCount=${ExternalSOP.readString(certs)}") + argCount += 1 + } + + override fun subjects(certs: InputStream): Boolean { + ExternalSOP.executeTransformingOperation( + Runtime.getRuntime(), commandList.plus(userId!!).plus(authorities), envList, certs) + .bytes + return true + } +} From ce19207a92f2a80c89bba6c4bdd4f3f0b163bf2b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 18:25:03 +0200 Subject: [PATCH 040/110] Show endOfOptions delimiter in help --- .../kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt | 7 ++----- .../kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt | 3 ++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt index 71ef79f..228809b 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt @@ -6,10 +6,8 @@ package sop.cli.picocli.commands import java.io.IOException import picocli.CommandLine.Command -import picocli.CommandLine.Model.CommandSpec import picocli.CommandLine.Option import picocli.CommandLine.Parameters -import picocli.CommandLine.Spec import sop.cli.picocli.SopCLI import sop.exception.SOPGPException.BadData import sop.exception.SOPGPException.UnsupportedOption @@ -17,11 +15,10 @@ import sop.exception.SOPGPException.UnsupportedOption @Command( name = "certify-userid", resourceBundle = "msg_certify-userid", - exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE, + showEndOfOptionsDelimiterInUsageHelp = true) class CertifyUserIdCmd : AbstractSopCmd() { - @Spec var spec: CommandSpec? = null - @Option(names = ["--no-armor"], negatable = true) var armor = true @Option(names = ["--userid"], required = true, arity = "1..*", paramLabel = "USERID") diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt index c2de148..da81a27 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt @@ -15,7 +15,8 @@ import sop.util.HexUtil.Companion.bytesToHex @Command( name = "validate-userid", resourceBundle = "msg_validate-userid", - exitCodeOnInvalidInput = SOPGPException.MissingArg.EXIT_CODE) + exitCodeOnInvalidInput = SOPGPException.MissingArg.EXIT_CODE, + showEndOfOptionsDelimiterInUsageHelp = true) class ValidateUserIdCmd : AbstractSopCmd() { @Option(names = ["--addr-spec-only"]) var addrSpecOnly: Boolean = false From eb712e6853c354184764e1a68152a7ac7641084a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 18:25:17 +0200 Subject: [PATCH 041/110] Add first test for new commands --- .../test/java/sop/cli/picocli/SOPTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) 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 fe49472..4d36322 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 @@ -13,10 +13,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import sop.SOP; import sop.exception.SOPGPException; import sop.operation.Armor; +import sop.operation.CertifyUserId; import sop.operation.ChangeKeyPassword; import sop.operation.Dearmor; import sop.operation.Decrypt; @@ -29,7 +31,10 @@ import sop.operation.InlineVerify; import sop.operation.DetachedSign; import sop.operation.DetachedVerify; import sop.operation.ListProfiles; +import sop.operation.MergeCerts; import sop.operation.RevokeKey; +import sop.operation.UpdateKey; +import sop.operation.ValidateUserId; import sop.operation.Version; public class SOPTest { @@ -52,6 +57,30 @@ public class SOPTest { @Test public void UnsupportedSubcommandsTest() { SOP nullCommandSOP = new SOP() { + @NotNull + @Override + public ValidateUserId validateUserId() { + return null; + } + + @NotNull + @Override + public CertifyUserId certifyUserId() { + return null; + } + + @NotNull + @Override + public MergeCerts mergeCerts() { + return null; + } + + @NotNull + @Override + public UpdateKey updateKey() { + return null; + } + @Override public Version version() { return null; @@ -140,6 +169,11 @@ public class SOPTest { commands.add(new String[] {"sign"}); commands.add(new String[] {"verify", "signature.asc", "cert.asc"}); commands.add(new String[] {"version"}); + commands.add(new String[] {"list-profiles", "generate-key"}); + commands.add(new String[] {"certify-userid", "--userid", "Alice ", "--", "alice.pgp"}); + commands.add(new String[] {"validate-userid", "Alice ", "bob.pgp", "--", "alice.pgp"}); + commands.add(new String[] {"update-key"}); + commands.add(new String[] {"merge-certs"}); for (String[] command : commands) { int exit = SopCLI.execute(command); From c7a40877635ea82acbc9479b64e6691a8c920ce0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 18:40:55 +0200 Subject: [PATCH 042/110] Fix nullability of sop commands --- .../test/java/sop/cli/picocli/SOPTest.java | 5 --- sop-java/src/main/kotlin/sop/SOP.kt | 34 +++++++++---------- sop-java/src/main/kotlin/sop/SOPV.kt | 8 ++--- 3 files changed, 21 insertions(+), 26 deletions(-) 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 4d36322..62c7581 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 @@ -13,7 +13,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import sop.SOP; import sop.exception.SOPGPException; @@ -57,25 +56,21 @@ public class SOPTest { @Test public void UnsupportedSubcommandsTest() { SOP nullCommandSOP = new SOP() { - @NotNull @Override public ValidateUserId validateUserId() { return null; } - @NotNull @Override public CertifyUserId certifyUserId() { return null; } - @NotNull @Override public MergeCerts mergeCerts() { return null; } - @NotNull @Override public UpdateKey updateKey() { return null; diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index c5f05e2..fbd0428 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -15,60 +15,60 @@ import sop.operation.* interface SOP : SOPV { /** Generate a secret key. */ - fun generateKey(): GenerateKey + fun generateKey(): GenerateKey? /** Extract a certificate (public key) from a secret key. */ - fun extractCert(): ExtractCert + fun extractCert(): ExtractCert? /** * Create detached signatures. If you want to sign a message inline, use [inlineSign] instead. */ - fun sign(): DetachedSign = detachedSign() + fun sign(): DetachedSign? = detachedSign() /** * Create detached signatures. If you want to sign a message inline, use [inlineSign] instead. */ - fun detachedSign(): DetachedSign + fun detachedSign(): DetachedSign? /** * Sign a message using inline signatures. If you need to create detached signatures, use * [detachedSign] instead. */ - fun inlineSign(): InlineSign + fun inlineSign(): InlineSign? /** Detach signatures from an inline signed message. */ - fun inlineDetach(): InlineDetach + fun inlineDetach(): InlineDetach? /** Encrypt a message. */ - fun encrypt(): Encrypt + fun encrypt(): Encrypt? /** Decrypt a message. */ - fun decrypt(): Decrypt + fun decrypt(): Decrypt? /** Convert binary OpenPGP data to ASCII. */ - fun armor(): Armor + fun armor(): Armor? /** Converts ASCII armored OpenPGP data to binary. */ - fun dearmor(): Dearmor + fun dearmor(): Dearmor? /** List supported [Profiles][Profile] of a subcommand. */ - fun listProfiles(): ListProfiles + fun listProfiles(): ListProfiles? /** Revoke one or more secret keys. */ - fun revokeKey(): RevokeKey + fun revokeKey(): RevokeKey? /** Update a key's password. */ - fun changeKeyPassword(): ChangeKeyPassword + fun changeKeyPassword(): ChangeKeyPassword? /** Keep a secret key up-to-date. */ - fun updateKey(): UpdateKey + fun updateKey(): UpdateKey? /** Merge OpenPGP certificates. */ - fun mergeCerts(): MergeCerts + fun mergeCerts(): MergeCerts? /** Certify OpenPGP Certificate User-IDs. */ - fun certifyUserId(): CertifyUserId + fun certifyUserId(): CertifyUserId? /** Validate a UserID in an OpenPGP certificate. */ - fun validateUserId(): ValidateUserId + fun validateUserId(): ValidateUserId? } diff --git a/sop-java/src/main/kotlin/sop/SOPV.kt b/sop-java/src/main/kotlin/sop/SOPV.kt index d331559..58a7f13 100644 --- a/sop-java/src/main/kotlin/sop/SOPV.kt +++ b/sop-java/src/main/kotlin/sop/SOPV.kt @@ -12,23 +12,23 @@ import sop.operation.Version interface SOPV { /** Get information about the implementations name and version. */ - fun version(): Version + fun version(): Version? /** * Verify detached signatures. If you need to verify an inline-signed message, use * [inlineVerify] instead. */ - fun verify(): DetachedVerify = detachedVerify() + fun verify(): DetachedVerify? = detachedVerify() /** * Verify detached signatures. If you need to verify an inline-signed message, use * [inlineVerify] instead. */ - fun detachedVerify(): DetachedVerify + fun detachedVerify(): DetachedVerify? /** * Verify signatures of an inline-signed message. If you need to verify detached signatures over * a message, use [detachedVerify] instead. */ - fun inlineVerify(): InlineVerify + fun inlineVerify(): InlineVerify? } From b2099dc508274f7eee8b5bf02b270ba46a2ead12 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 19:19:56 +0200 Subject: [PATCH 043/110] Document endOfOptionsDelimiter --- .../src/main/resources/msg_certify-userid.properties | 2 ++ .../src/main/resources/msg_certify-userid_de.properties | 2 ++ .../src/main/resources/msg_validate-userid.properties | 2 ++ .../src/main/resources/msg_validate-userid_de.properties | 2 ++ 4 files changed, 8 insertions(+) diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid.properties b/sop-java-picocli/src/main/resources/msg_certify-userid.properties index 252aae4..36dc6f4 100644 --- a/sop-java-picocli/src/main/resources/msg_certify-userid.properties +++ b/sop-java-picocli/src/main/resources/msg_certify-userid.properties @@ -14,6 +14,8 @@ standardInputDescription=Certificates that shall be certified standardOutput=CERTS standardOutputDescription=Certified certificates +picocli.endofoptions.description=End of options. Remainder are positional parameters. Fixes 'Missing required parameter' error + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties index 9f0a673..d634c59 100644 --- a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties +++ b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties @@ -12,6 +12,8 @@ KEYS[0..*]=Private Schl standardInputDescription=Zertifikate, auf denen Identitäten zertifiziert werden sollen standardOutputDescription=Zertifizierte Zertifikate +picocli.endofoptions.description=Ende der Optionen. Der Rest sind Positionsparameter. Behebt 'Missing required parameter' Fehler + # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 diff --git a/sop-java-picocli/src/main/resources/msg_validate-userid.properties b/sop-java-picocli/src/main/resources/msg_validate-userid.properties index 5cfed2d..d25fa3a 100644 --- a/sop-java-picocli/src/main/resources/msg_validate-userid.properties +++ b/sop-java-picocli/src/main/resources/msg_validate-userid.properties @@ -9,6 +9,8 @@ CERTS[1..*]=Authority OpenPGP certificates standardInput=CERTS standardInputDescription=OpenPGP certificates in which UserID bindings shall be validated +picocli.endofoptions.description=End of options. Remainder are positional parameters. Fixes 'Missing required parameter' error + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n diff --git a/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties b/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties index 8231c6a..f919465 100644 --- a/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties +++ b/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties @@ -9,6 +9,8 @@ CERTS[1..*]=Autorit standardInput=CERTS standardInputDescription=OpenPGP Zertifikate auf denen UserIDs validiert werden sollen +picocli.endofoptions.description=Ende der Optionen. Der Rest sind Positionsparameter. Behebt 'Missing required parameter' Fehler + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n From f98869d71bcbfd8e8a1d66a47bea2f6dc0989c1d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:26:36 +0200 Subject: [PATCH 044/110] Bump version --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 94d4c8d..7a18825 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '10.0.1' + shortVersion = '11.0.0' isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 From 69dd846667add743d381c644c419f5aaf75aabb2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:49:00 +0200 Subject: [PATCH 045/110] Fix documentation of merge-certs command --- sop-java-picocli/src/main/resources/msg_merge-certs.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sop-java-picocli/src/main/resources/msg_merge-certs.properties b/sop-java-picocli/src/main/resources/msg_merge-certs.properties index b01f577..8c0bfa3 100644 --- a/sop-java-picocli/src/main/resources/msg_merge-certs.properties +++ b/sop-java-picocli/src/main/resources/msg_merge-certs.properties @@ -2,7 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 usage.headerHeading=Merge OpenPGP certificates%n -usage.description=BLABLA +usage.header=Merge OpenPGP certificates from standard input with related elements from CERTS and emit the result to standard output +usage.description=Only certificates that were part of standard input will be emitted to standard output no-armor=ASCII armor the output CERTS[0..*]=OpenPGP certificates from which updates shall be merged into the base certificates from standard input From 1df57475493a18df3303f0162ec5c98259d53387 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:37:46 +0200 Subject: [PATCH 046/110] EncryptExternal: Fix parameter passing for --sign-with option --- .../src/main/kotlin/sop/external/operation/EncryptExternal.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt index 6f1cc6c..12d9cff 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt @@ -36,7 +36,7 @@ class EncryptExternal( override fun mode(mode: EncryptAs): Encrypt = apply { commandList.add("--as=$mode") } override fun signWith(key: InputStream): Encrypt = apply { - commandList.add("--sign-with@ENV:SIGN_WITH_$argCounter") + commandList.add("--sign-with=@ENV:SIGN_WITH_$argCounter") envList.add("SIGN_WITH_$argCounter=${ExternalSOP.readString(key)}") argCounter += 1 } From 4388f00dc08e936a402d79db52e3bdad47f0c71b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:00:30 +0200 Subject: [PATCH 047/110] Fix NPE in DecryptExternal when reading lines --- .../main/kotlin/sop/external/operation/DecryptExternal.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt index b68d3a6..e0a900d 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt @@ -108,8 +108,8 @@ class DecryptExternal( finish(process) val sessionKeyOutIn = FileInputStream(sessionKeyOut) - var line = readString(sessionKeyOutIn) - val sessionKey = SessionKey.fromString(line.trim { it <= ' ' }) + var line: String? = readString(sessionKeyOutIn) + val sessionKey = line?.let { l -> SessionKey.fromString(l.trim { it <= ' ' }) } sessionKeyOutIn.close() sessionKeyOut.delete() @@ -118,7 +118,7 @@ class DecryptExternal( val verifyOutIn = FileInputStream(verifyOut) val reader = BufferedReader(InputStreamReader(verifyOutIn)) while (reader.readLine().also { line = it } != null) { - verifications.add(Verification.fromString(line.trim())) + line?.let { verifications.add(Verification.fromString(it.trim())) } } reader.close() } From 65945e0094fa6a4a1e22cb0d6c71f0e745193c1e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 12:41:40 +0200 Subject: [PATCH 048/110] Fix external-sop decrypt --verifications-out --- .../src/main/kotlin/sop/external/operation/DecryptExternal.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt index e0a900d..1e6d6a2 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt @@ -78,7 +78,7 @@ class DecryptExternal( val verifyOut = File(tempDir, "verifications-out") verifyOut.delete() if (requireVerification) { - commandList.add("--verify-out=${verifyOut.absolutePath}") + commandList.add("--verifications-out=${verifyOut.absolutePath}") } try { From 2d4bc24c64b1a7e68a2e6596e5e07a63b940ccd5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 13:29:40 +0200 Subject: [PATCH 049/110] Abort tests on UnsupportedOption --- .../testsuite/operation/AbstractSOPTest.java | 5 +++++ .../sop/testsuite/AbortOnUnsupportedOption.kt | 12 +++++++++++ .../AbortOnUnsupportedOptionExtension.kt | 21 +++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt create mode 100644 sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java index 8595898..6c163f7 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java @@ -5,8 +5,11 @@ package sop.testsuite.operation; import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.provider.Arguments; import sop.SOP; +import sop.testsuite.AbortOnUnsupportedOption; +import sop.testsuite.AbortOnUnsupportedOptionExtension; import sop.testsuite.SOPInstanceFactory; import java.lang.reflect.InvocationTargetException; @@ -15,6 +18,8 @@ import java.util.List; import java.util.Map; import java.util.stream.Stream; +@ExtendWith(AbortOnUnsupportedOptionExtension.class) +@AbortOnUnsupportedOption public abstract class AbstractSOPTest { private static final List backends = new ArrayList<>(); diff --git a/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt b/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt new file mode 100644 index 0000000..cf99671 --- /dev/null +++ b/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite + +import java.lang.annotation.Inherited + +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +@Inherited +annotation class AbortOnUnsupportedOption diff --git a/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt b/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt new file mode 100644 index 0000000..809c78f --- /dev/null +++ b/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite + +import org.junit.jupiter.api.Assumptions +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.TestExecutionExceptionHandler +import sop.exception.SOPGPException + +class AbortOnUnsupportedOptionExtension : TestExecutionExceptionHandler { + override fun handleTestExecutionException(context: ExtensionContext, throwable: Throwable) { + val testClass = context.requiredTestClass + val annotation = testClass.getAnnotation(AbortOnUnsupportedOption::class.java) + if (annotation != null && SOPGPException.UnsupportedOption::class.isInstance(throwable)) { + Assumptions.assumeTrue(false, "Test aborted due to: " + throwable.message) + } + throw throwable + } +} From d25a424adc1328c9e10ffd72ac55c8f95188fdf5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:16:11 +0200 Subject: [PATCH 050/110] Update changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f188b0b..2c266f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 10.0.1-SNAPSHOT +- Remove `label()` option from `Armor` operation +- Fix exit code for 'Missing required option/parameter' error +- Fix `revoke-key`: Allow for multiple invocations of `--with-key-password` option + +## 10.0.0 +- Update implementation to [SOP Specification revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html). + - Throw `BadData` when passing KEYS where CERTS are expected + - Introduce `sopv` interface subset with revision `1.0` + - Add `sop version --sopv` + ## 8.0.1 - `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty. From a5232703959a9c6c1c1059d7052940044a518dad Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 21 Mar 2024 13:43:12 +0100 Subject: [PATCH 051/110] Update spec revision and badge link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa7e5a5..0efd41f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ SPDX-License-Identifier: Apache-2.0 # SOP for Java [![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java) -[![Spec Revision: 8](https://img.shields.io/badge/Spec%20Revision-8-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/08/) +[![Spec Revision: 10](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/10/) [![Coverage Status](https://coveralls.io/repos/github/pgpainless/sop-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/sop-java?branch=main) [![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/sop-java)](https://api.reuse.software/info/github.com/pgpainless/sop-java) From 8d7e89098fe462d2a8b5246f45ac5d05d2a21c70 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 13:40:25 +0200 Subject: [PATCH 052/110] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c266f1..2cb5b9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ SPDX-License-Identifier: Apache-2.0 - Remove `label()` option from `Armor` operation - Fix exit code for 'Missing required option/parameter' error - Fix `revoke-key`: Allow for multiple invocations of `--with-key-password` option +- Fix `EncryptExternal` use of `--sign-with` parameter +- Fix `NullPointerException` in `DecryptExternal` when reading lines +- Fix `DecryptExternal` use of `verifications-out` +- Test suite: Ignore tests if `UnsupportedOption` is thrown ## 10.0.0 - Update implementation to [SOP Specification revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html). From f7530e3263b69c73ca1c45b4e2ca3a3d182127b9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 13:43:45 +0200 Subject: [PATCH 053/110] Bump logback to 1.4.14 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 80c8092..7572c34 100644 --- a/version.gradle +++ b/version.gradle @@ -12,7 +12,7 @@ allprojects { jsrVersion = '3.0.2' junitVersion = '5.8.2' junitSysExitVersion = '1.1.2' - logbackVersion = '1.2.11' + logbackVersion = '1.4.14' mockitoVersion = '4.5.1' picocliVersion = '4.6.3' slf4jVersion = '1.7.36' From 261ac212b8f91ef58557f8e09278edd2dfc6ac81 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 13:44:17 +0200 Subject: [PATCH 054/110] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cb5b9d..37c38fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ SPDX-License-Identifier: Apache-2.0 - Fix `NullPointerException` in `DecryptExternal` when reading lines - Fix `DecryptExternal` use of `verifications-out` - Test suite: Ignore tests if `UnsupportedOption` is thrown +- Bump `logback-core` to `1.4.14` ## 10.0.0 - Update implementation to [SOP Specification revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html). From 354ef8841aafe4aa0a66c76e70d5db5208223e7d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 13:46:48 +0200 Subject: [PATCH 055/110] SOP-Java 10.0.1 --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 7572c34..fbac859 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '10.0.0' - isSnapshot = true + shortVersion = '10.0.1' + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 7014dbcfb7bec164cf8e216eaeca09ca6ca56bf5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 14:36:41 +0200 Subject: [PATCH 056/110] SOP-Java 10.0.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index fbac859..a018d80 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '10.0.1' - isSnapshot = false + shortVersion = '10.0.2' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 63d80452247a4f596eb49904ee57028092a7cdd7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 14:41:34 +0200 Subject: [PATCH 057/110] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37c38fc..ff1f4cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 10.0.1-SNAPSHOT +## 10.0.1 - Remove `label()` option from `Armor` operation - Fix exit code for 'Missing required option/parameter' error - Fix `revoke-key`: Allow for multiple invocations of `--with-key-password` option From a90f9be0e44144e5545d0422455ef4cd85373f68 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 15:50:31 +0200 Subject: [PATCH 058/110] Downgrade logback-core to 1.2.13 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index a018d80..65a8a27 100644 --- a/version.gradle +++ b/version.gradle @@ -12,7 +12,7 @@ allprojects { jsrVersion = '3.0.2' junitVersion = '5.8.2' junitSysExitVersion = '1.1.2' - logbackVersion = '1.4.14' + logbackVersion = '1.2.13' mockitoVersion = '4.5.1' picocliVersion = '4.6.3' slf4jVersion = '1.7.36' From a09f10fe8547d906030bc11aab0f1fd0d48fed2b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 15:50:40 +0200 Subject: [PATCH 059/110] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff1f4cc..493a3b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 10.0.2-SNAPSHOT +- Downgrade `logback-core` to `1.2.13` + ## 10.0.1 - Remove `label()` option from `Armor` operation - Fix exit code for 'Missing required option/parameter' error From 1958614fac828a52519f34a78429ac6e92c3d935 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 16:20:33 +0200 Subject: [PATCH 060/110] SOP-Java 10.0.2 --- CHANGELOG.md | 2 +- version.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 493a3b9..6c0c0c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 10.0.2-SNAPSHOT +## 10.0.2 - Downgrade `logback-core` to `1.2.13` ## 10.0.1 diff --git a/version.gradle b/version.gradle index 65a8a27..256871d 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '10.0.2' - isSnapshot = true + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From b3f446fe8d49aad790b7f0ed264d3491ff0a0d22 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 16:22:06 +0200 Subject: [PATCH 061/110] SOP-Java 10.0.3-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 256871d..db61bcc 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '10.0.2' - isSnapshot = false + shortVersion = '10.0.3' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 42a16a4f6d46199db279f6daf9a9b70859841b7b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Oct 2024 13:15:06 +0100 Subject: [PATCH 062/110] Fix password parameter passing in change-key-password --- .../sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt index 0c2eb4a..be37309 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt @@ -33,9 +33,15 @@ class ChangeKeyPasswordCmd : AbstractSopCmd() { changeKeyPassword.noArmor() } - oldKeyPasswords.forEach { changeKeyPassword.oldKeyPassphrase(it) } + oldKeyPasswords.forEach { + val password = stringFromInputStream(getInput(it)) + changeKeyPassword.oldKeyPassphrase(password) + } - newKeyPassword?.let { changeKeyPassword.newKeyPassphrase(it) } + newKeyPassword?.let { + val password = stringFromInputStream(getInput(it)) + changeKeyPassword.newKeyPassphrase(password) + } try { changeKeyPassword.keys(System.`in`).writeTo(System.out) From 375dd6578903eb23cac395fe86b861a86afbce60 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:13:12 +0200 Subject: [PATCH 063/110] revoke-key command: Allow for multiple '--with-key-password' options --- .../main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt index 0b93ac5..b9b1015 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt @@ -19,8 +19,8 @@ class RevokeKeyCmd : AbstractSopCmd() { @Option(names = ["--no-armor"], negatable = true) var armor = true - @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") - var withKeyPassword: String? = null + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD", arity = "0..*") + var withKeyPassword: List = listOf() override fun run() { val revokeKey = throwIfUnsupportedSubcommand(SopCLI.getSop().revokeKey(), "revoke-key") @@ -29,9 +29,9 @@ class RevokeKeyCmd : AbstractSopCmd() { revokeKey.noArmor() } - withKeyPassword?.let { + for (passwordIn in withKeyPassword) { try { - val password = stringFromInputStream(getInput(it)) + val password = stringFromInputStream(getInput(passwordIn)) revokeKey.withKeyPassword(password) } catch (e: SOPGPException.UnsupportedOption) { val errorMsg = From f35fd6c1ae39cd9cf2b79bd0d5a541c191203e11 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Oct 2024 13:53:57 +0100 Subject: [PATCH 064/110] Update changelog --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c0c0c1..13a87c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 10.0.3-SNAPSHOT +- CLI `change-key-password`: Fix indirect parameter passing for new and old passwords (thanks to @dkg for the report) +- Backport: `revoke-key`: Allow for multiple password options + ## 10.0.2 - Downgrade `logback-core` to `1.2.13` @@ -25,6 +29,10 @@ SPDX-License-Identifier: Apache-2.0 - Introduce `sopv` interface subset with revision `1.0` - Add `sop version --sopv` +## 8.0.2 +- CLI `change-key-password`: Fix indirect parameter passing for new and old passwords (thanks to @dkg for the report) +- Backport: `revoke-key`: Allow for multiple password options + ## 8.0.1 - `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty. @@ -43,6 +51,13 @@ SPDX-License-Identifier: Apache-2.0 - Change `EncryptAs` values into lowercase - Change `SignAs` values into lowercase +## 7.0.2 +- CLI `change-key-password`: Fix indirect parameter passing for new and old passwords (thanks to @dkg for the report) +- Backport: revoke-key command: Allow for multiple '--with-key-password' options + +## 7.0.1 +- `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty. + ## 7.0.0 - Update implementation to [SOP Specification revision 07](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-07.html). - Add support for new `revoke-key` subcommand From c136d40fa7de9fa7e1979b1ffdd36f1fe44c9474 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Oct 2024 13:54:31 +0100 Subject: [PATCH 065/110] SOP-Java 10.0.3 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index db61bcc..396af1a 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '10.0.3' - isSnapshot = true + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From e6c9d6f43d3a37f0e9b89462d5f13a004b2c85f2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Oct 2024 14:06:37 +0100 Subject: [PATCH 066/110] SOP-Java 10.0.4-SNAPSHOT --- CHANGELOG.md | 2 +- version.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13a87c3..feee975 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 10.0.3-SNAPSHOT +## 10.0.3 - CLI `change-key-password`: Fix indirect parameter passing for new and old passwords (thanks to @dkg for the report) - Backport: `revoke-key`: Allow for multiple password options diff --git a/version.gradle b/version.gradle index 396af1a..e8bcbef 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '10.0.3' - isSnapshot = false + shortVersion = '10.0.4' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From ac00b68694a62fa50c67717910dc59fb0d7e4e0a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 21 Mar 2024 13:43:25 +0100 Subject: [PATCH 067/110] Add description of external-sop module --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0efd41f..baeb874 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ The repository contains the following modules: * [sop-java](/sop-java) defines a set of Java interfaces describing the Stateless OpenPGP Protocol. * [sop-java-picocli](/sop-java-picocli) contains a wrapper application that transforms the `sop-java` API into a command line application compatible with the SOP-CLI specification. +* [external-sop](/external-sop) contains an API implementation that can be used to forward API calls to a SOP executable, +allowing to delegate the implementation logic to an arbitrary SOP CLI implementation. ## Known Implementations (Please expand!) From e7778cb0d299ae5c479b538d29f8e639c71be2e7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 27 Mar 2024 21:50:01 +0100 Subject: [PATCH 068/110] Remove deprecated junit5-system-exit Replaced with custom test DSL that avoids System.exit --- sop-java-picocli/build.gradle | 5 +- .../test/java/sop/cli/picocli/SOPTest.java | 11 +- .../cli/picocli/commands/ArmorCmdTest.java | 12 +- .../cli/picocli/commands/DearmorCmdTest.java | 8 +- .../cli/picocli/commands/DecryptCmdTest.java | 137 +++++----- .../cli/picocli/commands/EncryptCmdTest.java | 121 +++++---- .../picocli/commands/ExtractCertCmdTest.java | 22 +- .../picocli/commands/GenerateKeyCmdTest.java | 30 ++- .../picocli/commands/InlineDetachCmdTest.java | 12 +- .../sop/cli/picocli/commands/SignCmdTest.java | 61 +++-- .../cli/picocli/commands/VerifyCmdTest.java | 70 ++++-- .../cli/picocli/commands/VersionCmdTest.java | 41 ++- .../assertions/SopExecutionAssertions.java | 235 ++++++++++++++++++ 13 files changed, 550 insertions(+), 215 deletions(-) create mode 100644 sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java diff --git a/sop-java-picocli/build.gradle b/sop-java-picocli/build.gradle index 438ef50..0596ad3 100644 --- a/sop-java-picocli/build.gradle +++ b/sop-java-picocli/build.gradle @@ -12,15 +12,12 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - // Testing Exit Codes in JUnit - // https://todd.ginsberg.com/post/testing-system-exit/ - testImplementation "com.ginsberg:junit5-system-exit:$junitSysExitVersion" - // Mocking Components testImplementation "org.mockito:mockito-core:$mockitoVersion" // SOP implementation(project(":sop-java")) + testImplementation(testFixtures(project(":sop-java"))) // CLI implementation "info.picocli:picocli:$picocliVersion" 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 68b32be..fe49472 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 @@ -6,12 +6,13 @@ package sop.cli.picocli; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; +import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedSubcommand; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.Test; import sop.SOP; import sop.exception.SOPGPException; @@ -34,20 +35,18 @@ import sop.operation.Version; public class SOPTest { @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedSubcommand.EXIT_CODE) public void assertExitOnInvalidSubcommand() { SOP sop = mock(SOP.class); SopCLI.setSopInstance(sop); - SopCLI.main(new String[] {"invalid"}); + assertUnsupportedSubcommand(() -> SopCLI.execute("invalid")); } @Test - @ExpectSystemExitWithStatus(1) public void assertThrowsIfNoSOPBackendSet() { SopCLI.setSopInstance(null); - // At this point, no SOP backend is set, so an InvalidStateException triggers exit(1) - SopCLI.main(new String[] {"armor"}); + // At this point, no SOP backend is set, so an InvalidStateException triggers error code 1 + assertGenericError(() -> SopCLI.execute("armor")); } @Test diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java index da211e0..3dd4c7c 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java @@ -4,8 +4,6 @@ package sop.cli.picocli.commands; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import com.ginsberg.junit.exit.FailOnSystemExit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.Ready; @@ -24,6 +22,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; public class ArmorCmdTest { @@ -42,24 +42,22 @@ public class ArmorCmdTest { @Test public void assertDataIsAlwaysCalled() throws SOPGPException.BadData, IOException { - SopCLI.main(new String[] {"armor"}); + assertSuccess(() -> SopCLI.execute("armor")); verify(armor, times(1)).data((InputStream) any()); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void ifBadDataExit41() throws SOPGPException.BadData, IOException { when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"armor"}); + assertBadData(() -> SopCLI.execute("armor")); } @Test - @FailOnSystemExit public void ifNoErrorsNoExit() { when(sop.armor()).thenReturn(armor); - SopCLI.main(new String[] {"armor"}); + assertSuccess(() -> SopCLI.execute("armor")); } private static Ready nopReady() { diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java index 875eaed..b0a9fd8 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java @@ -9,12 +9,13 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.Ready; @@ -48,14 +49,13 @@ public class DearmorCmdTest { @Test public void assertDataIsCalled() throws IOException, SOPGPException.BadData { - SopCLI.main(new String[] {"dearmor"}); + assertSuccess(() -> SopCLI.execute("dearmor")); verify(dearmor, times(1)).data((InputStream) any()); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void assertBadDataCausesExit41() throws IOException, SOPGPException.BadData { when(dearmor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException("invalid armor"))); - SopCLI.main(new String[] {"dearmor"}); + assertBadData(() -> SopCLI.execute("dearmor")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java index 62070c2..b7cb8bc 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java @@ -4,7 +4,6 @@ package sop.cli.picocli.commands; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatcher; @@ -42,6 +41,18 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertCannotDecrypt; +import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; +import static sop.testsuite.assertions.SopExecutionAssertions.assertIncompleteVerification; +import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyIsProtected; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput; +import static sop.testsuite.assertions.SopExecutionAssertions.assertOutputExists; +import static sop.testsuite.assertions.SopExecutionAssertions.assertPasswordNotHumanReadable; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedAsymmetricAlgo; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption; public class DecryptCmdTest { @@ -74,47 +85,47 @@ public class DecryptCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void missingArgumentsExceptionCausesExit19() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException { when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.MissingArg("Missing arguments.")); - SopCLI.main(new String[] {"decrypt"}); + assertMissingArg(() -> SopCLI.execute("decrypt")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void badDataExceptionCausesExit41() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException { when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"decrypt"}); + assertBadData(() -> SopCLI.execute("decrypt")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.PasswordNotHumanReadable.EXIT_CODE) public void assertNotHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable"); when(decrypt.withPassword(any())).thenThrow(new SOPGPException.PasswordNotHumanReadable()); - SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertPasswordNotHumanReadable(() -> + SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath()) + ); } @Test public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("orange"); - SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertSuccess(() -> SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath())); verify(decrypt, times(1)).withPassword("orange"); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("swordfish"); when(decrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Decrypting with password not supported.")); - SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath()) + ); } @Test public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption { Date now = new Date(); - SopCLI.main(new String[] {"decrypt"}); + assertSuccess(() -> SopCLI.execute("decrypt")); verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME); verify(decrypt, times(1)).verifyNotAfter( ArgumentMatchers.argThat(argument -> { @@ -125,7 +136,8 @@ public class DecryptCmdTest { @Test public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"decrypt", "--verify-not-before", "-", "--verify-not-after", "-"}); + assertSuccess(() -> + SopCLI.execute("decrypt", "--verify-not-before", "-", "--verify-not-after", "-")); verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME); verify(decrypt, times(1)).verifyNotAfter(AbstractSopCmd.END_OF_TIME); } @@ -138,54 +150,57 @@ public class DecryptCmdTest { return Math.abs(now.getTime() - argument.getTime()) <= 1000; }; - SopCLI.main(new String[] {"decrypt", "--verify-not-before", "now", "--verify-not-after", "now"}); + assertSuccess(() -> + SopCLI.execute("decrypt", "--verify-not-before", "now", "--verify-not-after", "now")); verify(decrypt, times(1)).verifyNotAfter(ArgumentMatchers.argThat(isMaxOneSecOff)); verify(decrypt, times(1)).verifyNotBefore(ArgumentMatchers.argThat(isMaxOneSecOff)); } @Test - @ExpectSystemExitWithStatus(1) public void assertMalformedDateInNotBeforeCausesExit1() { // ParserException causes exit(1) - SopCLI.main(new String[] {"decrypt", "--verify-not-before", "invalid"}); + assertGenericError(() -> + SopCLI.execute("decrypt", "--verify-not-before", "invalid")); } @Test - @ExpectSystemExitWithStatus(1) public void assertMalformedDateInNotAfterCausesExit1() { // ParserException causes exit(1) - SopCLI.main(new String[] {"decrypt", "--verify-not-after", "invalid"}); + assertGenericError(() -> + SopCLI.execute("decrypt", "--verify-not-after", "invalid")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertUnsupportedNotAfterCausesExit37() throws SOPGPException.UnsupportedOption { - when(decrypt.verifyNotAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); - SopCLI.main(new String[] {"decrypt", "--verify-not-after", "now"}); + when(decrypt.verifyNotAfter(any())).thenThrow( + new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); + assertUnsupportedOption(() -> + SopCLI.execute("decrypt", "--verify-not-after", "now")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertUnsupportedNotBeforeCausesExit37() throws SOPGPException.UnsupportedOption { - when(decrypt.verifyNotBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); - SopCLI.main(new String[] {"decrypt", "--verify-not-before", "now"}); + when(decrypt.verifyNotBefore(any())).thenThrow( + new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); + assertUnsupportedOption(() -> + SopCLI.execute("decrypt", "--verify-not-before", "now")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE) public void assertExistingSessionKeyOutFileCausesExit59() throws IOException { File tempFile = File.createTempFile("existing-session-key-", ".tmp"); tempFile.deleteOnExit(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); + assertOutputExists(() -> + SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertWhenSessionKeyCannotBeExtractedExit37() throws IOException { Path tempDir = Files.createTempDirectory("session-key-out-dir"); File tempFile = new File(tempDir.toFile(), "session-key"); tempFile.deleteOnExit(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath())); } @Test @@ -210,8 +225,10 @@ public class DecryptCmdTest { File verificationsFile = new File(tempDir.toFile(), "verifications"); File keyFile = new File(tempDir.toFile(), "key.asc"); keyFile.createNewFile(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(), - "--verifications-out", verificationsFile.getAbsolutePath(), "--verify-with", keyFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(), + "--verifications-out", verificationsFile.getAbsolutePath(), "--verify-with", + keyFile.getAbsolutePath())); ByteArrayOutputStream bytesInFile = new ByteArrayOutputStream(); try (FileInputStream fileIn = new FileInputStream(sessionKeyFile)) { @@ -241,10 +258,10 @@ public class DecryptCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.CannotDecrypt.EXIT_CODE) public void assertUnableToDecryptExceptionResultsInExit29() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.CannotDecrypt()); - SopCLI.main(new String[] {"decrypt"}); + assertCannotDecrypt(() -> + SopCLI.execute("decrypt")); } @Test @@ -258,30 +275,32 @@ public class DecryptCmdTest { return new DecryptionResult(null, Collections.emptyList()); } }); - SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath(), "--verifications-out", verifyOut.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("decrypt", "--verify-with", tempFile.getAbsolutePath(), "--verifications-out", + verifyOut.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void badDataInVerifyWithCausesExit41() throws IOException, SOPGPException.BadData { when(decrypt.verifyWithCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File tempFile = File.createTempFile("verify-with-", ".tmp"); - SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("decrypt", "--verify-with", tempFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void unexistentCertFileCausesExit61() { - SopCLI.main(new String[] {"decrypt", "--verify-with", "invalid"}); + assertMissingInput(() -> + SopCLI.execute("decrypt", "--verify-with", "invalid")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE) public void existingVerifyOutCausesExit59() throws IOException { File certFile = File.createTempFile("existing-verify-out-cert", ".asc"); File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp"); - SopCLI.main(new String[] {"decrypt", "--verifications-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); + assertOutputExists(() -> SopCLI.execute("decrypt", "--verifications-out", + existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath())); } @Test @@ -305,7 +324,9 @@ public class DecryptCmdTest { } }); - SopCLI.main(new String[] {"decrypt", "--verifications-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("decrypt", "--verifications-out", verifyOut.getAbsolutePath(), + "--verify-with", certFile.getAbsolutePath())); try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) { String line = reader.readLine(); assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line); @@ -320,66 +341,64 @@ public class DecryptCmdTest { File sessionKeyFile1 = TestFileUtil.writeTempStringFile(key1.toString()); File sessionKeyFile2 = TestFileUtil.writeTempStringFile(key2.toString()); - SopCLI.main(new String[] {"decrypt", - "--with-session-key", sessionKeyFile1.getAbsolutePath(), - "--with-session-key", sessionKeyFile2.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("decrypt", + "--with-session-key", sessionKeyFile1.getAbsolutePath(), + "--with-session-key", sessionKeyFile2.getAbsolutePath())); verify(decrypt).withSessionKey(key1); verify(decrypt).withSessionKey(key2); } @Test - @ExpectSystemExitWithStatus(1) public void assertMalformedSessionKeysResultInExit1() throws IOException { File sessionKeyFile = TestFileUtil.writeTempStringFile("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137"); - SopCLI.main(new String[] {"decrypt", - "--with-session-key", sessionKeyFile.getAbsolutePath()}); + assertGenericError(() -> + SopCLI.execute("decrypt", + "--with-session-key", sessionKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void assertBadDataInKeysResultsInExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); + assertBadData(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void assertKeyFileNotFoundCausesExit61() { - SopCLI.main(new String[] {"decrypt", "nonexistent-key"}); + assertMissingInput(() -> SopCLI.execute("decrypt", "nonexistent-key")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE) public void assertProtectedKeyCausesExit67() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); + assertKeyIsProtected(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) public void assertUnsupportedAlgorithmExceptionCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new IOException())); File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); + assertUnsupportedAsymmetricAlgo(() -> + SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void assertMissingPassphraseFileCausesExit61() { - SopCLI.main(new String[] {"decrypt", "--with-password", "missing"}); + assertMissingInput(() -> + SopCLI.execute("decrypt", "--with-password", "missing")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void assertMissingSessionKeyFileCausesExit61() { - SopCLI.main(new String[] {"decrypt", "--with-session-key", "missing"}); + assertMissingInput(() -> + SopCLI.execute("decrypt", "--with-session-key", "missing")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.IncompleteVerification.EXIT_CODE) public void verifyOutWithoutVerifyWithCausesExit23() { - SopCLI.main(new String[] {"decrypt", "--verifications-out", "out.file"}); + assertIncompleteVerification(() -> + SopCLI.execute("decrypt", "--verifications-out", "out.file")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java index 09346af..85ae052 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java @@ -4,7 +4,6 @@ package sop.cli.picocli.commands; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,6 +27,17 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertCertCannotEncrypt; +import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; +import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyCannotSign; +import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyIsProtected; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput; +import static sop.testsuite.assertions.SopExecutionAssertions.assertPasswordNotHumanReadable; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedAsymmetricAlgo; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption; public class EncryptCmdTest { @@ -50,48 +60,50 @@ public class EncryptCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) - public void missingBothPasswordAndCertFileCauseExit19() { - SopCLI.main(new String[] {"encrypt", "--no-armor"}); + public void missingBothPasswordAndCertFileCausesMissingArg() { + assertMissingArg(() -> + SopCLI.execute("encrypt", "--no-armor")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void as_unsupportedEncryptAsCausesExit37() throws SOPGPException.UnsupportedOption { + public void as_unsupportedEncryptAsCausesUnsupportedOption() throws SOPGPException.UnsupportedOption { when(encrypt.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting encryption mode not supported.")); - SopCLI.main(new String[] {"encrypt", "--as", "Binary"}); + assertUnsupportedOption(() -> + SopCLI.execute("encrypt", "--as", "Binary")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void as_invalidModeOptionCausesExit37() { - SopCLI.main(new String[] {"encrypt", "--as", "invalid"}); + public void as_invalidModeOptionCausesUnsupportedOption() { + assertUnsupportedOption(() -> + SopCLI.execute("encrypt", "--as", "invalid")); } @Test public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("0rbit"); for (EncryptAs mode : EncryptAs.values()) { - SopCLI.main(new String[] {"encrypt", "--as", mode.name(), "--with-password", passwordFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("encrypt", "--as", mode.name(), + "--with-password", passwordFile.getAbsolutePath())); verify(encrypt, times(1)).mode(mode); } } @Test - @ExpectSystemExitWithStatus(SOPGPException.PasswordNotHumanReadable.EXIT_CODE) - public void withPassword_notHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { + public void withPassword_notHumanReadablePasswordCausesPWNotHumanReadable() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable()); File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertPasswordNotHumanReadable(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void withPassword_unsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { + public void withPassword_unsupportedWithPasswordCausesUnsupportedOption() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported.")); File passwordFile = TestFileUtil.writeTempStringFile("orange"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); } @Test @@ -99,99 +111,107 @@ public class EncryptCmdTest { File keyFile1 = File.createTempFile("sign-with-1-", ".asc"); File keyFile2 = File.createTempFile("sign-with-2-", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("password"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile1.getAbsolutePath(), "--sign-with", keyFile2.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), + "--sign-with", keyFile1.getAbsolutePath(), + "--sign-with", keyFile2.getAbsolutePath())); verify(encrypt, times(2)).signWith((InputStream) any()); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) - public void signWith_nonExistentKeyFileCausesExit61() { - SopCLI.main(new String[] {"encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"}); + public void signWith_nonExistentKeyFileCausesMissingInput() { + assertMissingInput(() -> + SopCLI.execute("encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE) - public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + public void signWith_keyIsProtectedCausesKeyIsProtected() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); File keyFile = File.createTempFile("sign-with", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("starship"); - SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", passwordFile.getAbsolutePath()}); + assertKeyIsProtected(() -> + SopCLI.execute("encrypt", "--sign-with", keyFile.getAbsolutePath(), + "--with-password", passwordFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) - public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + public void signWith_unsupportedAsymmetricAlgoCausesUnsupportedAsymAlgo() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); File keyFile = File.createTempFile("sign-with", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("123456"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); + assertUnsupportedAsymmetricAlgo(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), + "--sign-with", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyCannotSign.EXIT_CODE) - public void signWith_certCannotSignCausesExit79() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { + public void signWith_certCannotSignCausesKeyCannotSign() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign()); File keyFile = File.createTempFile("sign-with", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("dragon"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); + assertKeyCannotSign(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), + "--sign-with", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) - public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + public void signWith_badDataCausesBadData() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File keyFile = File.createTempFile("sign-with", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("orange"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), + "--sign-with", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) - public void cert_nonExistentCertFileCausesExit61() { - SopCLI.main(new String[] {"encrypt", "invalid.asc"}); + public void cert_nonExistentCertFileCausesMissingInput() { + assertMissingInput(() -> + SopCLI.execute("encrypt", "invalid.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) - public void cert_unsupportedAsymmetricAlgorithmCausesExit13() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { + public void cert_unsupportedAsymmetricAlgorithmCausesUnsupportedAsymAlg() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); + assertUnsupportedAsymmetricAlgo(() -> + SopCLI.execute("encrypt", certFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.CertCannotEncrypt.EXIT_CODE) - public void cert_certCannotEncryptCausesExit17() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { + public void cert_certCannotEncryptCausesCertCannotEncrypt() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.CertCannotEncrypt("Certificate cannot encrypt.", new Exception())); File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); + assertCertCannotEncrypt(() -> + SopCLI.execute("encrypt", certFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) - public void cert_badDataCausesExit41() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { + public void cert_badDataCausesBadData() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("encrypt", certFile.getAbsolutePath())); } @Test public void noArmor_notCalledByDefault() throws IOException { File passwordFile = TestFileUtil.writeTempStringFile("clownfish"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); verify(encrypt, never()).noArmor(); } @Test public void noArmor_callGetsPassedDown() throws IOException { File passwordFile = TestFileUtil.writeTempStringFile("monkey"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--no-armor"}); + assertSuccess(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), "--no-armor")); verify(encrypt, times(1)).noArmor(); } @Test - @ExpectSystemExitWithStatus(1) - public void writeTo_ioExceptionCausesExit1() throws IOException { + public void writeTo_ioExceptionCausesGenericError() throws IOException { when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult() { @Override public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException { @@ -199,6 +219,7 @@ public class EncryptCmdTest { } }); File passwordFile = TestFileUtil.writeTempStringFile("wildcat"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertGenericError(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java index 12f837d..3b046a0 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java @@ -10,12 +10,14 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.Ready; @@ -45,32 +47,34 @@ public class ExtractCertCmdTest { @Test public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"extract-cert"}); + assertSuccess(() -> + SopCLI.execute("extract-cert")); verify(extractCert, never()).noArmor(); } @Test public void noArmor_passedDown() { - SopCLI.main(new String[] {"extract-cert", "--no-armor"}); + assertSuccess(() -> + SopCLI.execute("extract-cert", "--no-armor")); verify(extractCert, times(1)).noArmor(); } @Test - @ExpectSystemExitWithStatus(1) - public void key_ioExceptionCausesExit1() throws IOException, SOPGPException.BadData { + public void key_ioExceptionCausesGenericError() throws IOException, SOPGPException.BadData { when(extractCert.key((InputStream) any())).thenReturn(new Ready() { @Override public void writeTo(OutputStream outputStream) throws IOException { throw new IOException(); } }); - SopCLI.main(new String[] {"extract-cert"}); + assertGenericError(() -> + SopCLI.execute("extract-cert")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) - public void key_badDataCausesExit41() throws IOException, SOPGPException.BadData { + public void key_badDataCausesBadData() throws IOException, SOPGPException.BadData { when(extractCert.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"extract-cert"}); + assertBadData(() -> + SopCLI.execute("extract-cert")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java index e7ebf1a..126c851 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java @@ -10,11 +10,14 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedAsymmetricAlgo; import java.io.IOException; import java.io.OutputStream; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InOrder; @@ -47,19 +50,22 @@ public class GenerateKeyCmdTest { @Test public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"generate-key", "Alice"}); + assertSuccess(() -> + SopCLI.execute("generate-key", "Alice")); verify(generateKey, never()).noArmor(); } @Test public void noArmor_passedDown() { - SopCLI.main(new String[] {"generate-key", "--no-armor", "Alice"}); + assertSuccess(() -> + SopCLI.execute("generate-key", "--no-armor", "Alice")); verify(generateKey, times(1)).noArmor(); } @Test public void userId_multipleUserIdsPassedDownInProperOrder() { - SopCLI.main(new String[] {"generate-key", "Alice ", "Bob "}); + assertSuccess(() -> + SopCLI.execute("generate-key", "Alice ", "Bob ")); InOrder inOrder = Mockito.inOrder(generateKey); inOrder.verify(generateKey).userId("Alice "); @@ -69,30 +75,32 @@ public class GenerateKeyCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void missingArgumentCausesExit19() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { // TODO: RFC4880-bis and the current Stateless OpenPGP CLI spec allow keys to have no user-ids, // so we might want to change this test in the future. when(generateKey.generate()).thenThrow(new SOPGPException.MissingArg("Missing user-id.")); - SopCLI.main(new String[] {"generate-key"}); + assertMissingArg(() -> + SopCLI.execute("generate-key")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) public void unsupportedAsymmetricAlgorithmCausesExit13() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { when(generateKey.generate()).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); - SopCLI.main(new String[] {"generate-key", "Alice"}); + assertUnsupportedAsymmetricAlgo(() -> + SopCLI.execute("generate-key", "Alice")); + } @Test - @ExpectSystemExitWithStatus(1) - public void ioExceptionCausesExit1() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { + public void ioExceptionCausesGenericError() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { when(generateKey.generate()).thenReturn(new Ready() { @Override public void writeTo(OutputStream outputStream) throws IOException { throw new IOException(); } }); - SopCLI.main(new String[] {"generate-key", "Alice"}); + + assertGenericError(() -> + SopCLI.execute("generate-key", "Alice")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java index 3a16c61..a230aaa 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java @@ -4,7 +4,6 @@ package sop.cli.picocli.commands; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.ReadyWithResult; @@ -26,6 +25,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; public class InlineDetachCmdTest { @@ -41,9 +42,9 @@ public class InlineDetachCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) - public void testMissingSignaturesOutResultsInExit19() { - SopCLI.main(new String[] {"inline-detach"}); + public void testMissingSignaturesOutResultsInMissingArg() { + assertMissingArg(() -> + SopCLI.execute("inline-detach")); } @Test @@ -67,7 +68,8 @@ public class InlineDetachCmdTest { } }); - SopCLI.main(new String[] {"inline-detach", "--signatures-out", tempFile.getAbsolutePath(), "--no-armor"}); + assertSuccess(() -> + SopCLI.execute("inline-detach", "--signatures-out", tempFile.getAbsolutePath(), "--no-armor")); verify(inlineDetach, times(1)).noArmor(); verify(inlineDetach, times(1)).message((InputStream) any()); } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java index c3d6b59..324d39a 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java @@ -10,13 +10,20 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertExpectedText; +import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; +import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyIsProtected; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.ReadyWithResult; @@ -54,70 +61,77 @@ public class SignCmdTest { @Test public void as_optionsAreCaseInsensitive() { - SopCLI.main(new String[] {"sign", "--as", "Binary", keyFile.getAbsolutePath()}); - SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); - SopCLI.main(new String[] {"sign", "--as", "BINARY", keyFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("sign", "--as", "Binary", keyFile.getAbsolutePath())); + assertSuccess(() -> + SopCLI.execute("sign", "--as", "binary", keyFile.getAbsolutePath())); + assertSuccess(() -> + SopCLI.execute("sign", "--as", "BINARY", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void as_invalidOptionCausesExit37() { - SopCLI.main(new String[] {"sign", "--as", "Invalid", keyFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("sign", "--as", "Invalid", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { when(detachedSign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting signing mode not supported.")); - SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("sign", "--as", "binary", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void key_nonExistentKeyFileCausesExit61() { - SopCLI.main(new String[] {"sign", "invalid.asc"}); + assertMissingInput(() -> + SopCLI.execute("sign", "invalid.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE) public void key_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertKeyIsProtected(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void key_missingKeyFileCausesExit19() { - SopCLI.main(new String[] {"sign"}); + assertMissingArg(() -> + SopCLI.execute("sign")); } @Test public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); verify(detachedSign, never()).noArmor(); } @Test public void noArmor_passedDown() { - SopCLI.main(new String[] {"sign", "--no-armor", keyFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("sign", "--no-armor", keyFile.getAbsolutePath())); verify(detachedSign, times(1)).noArmor(); } @Test public void withKeyPassword_passedDown() { - SopCLI.main(new String[] {"sign", "--with-key-password", passFile.getAbsolutePath(), keyFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("sign", + "--with-key-password", passFile.getAbsolutePath(), + keyFile.getAbsolutePath())); verify(detachedSign, times(1)).withKeyPassword("sw0rdf1sh"); } @Test - @ExpectSystemExitWithStatus(1) public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText { when(detachedSign.data((InputStream) any())).thenReturn(new ReadyWithResult() { @Override @@ -125,13 +139,14 @@ public class SignCmdTest { throw new IOException(); } }); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertGenericError(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.ExpectedText.EXIT_CODE) public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText { when(detachedSign.data((InputStream) any())).thenThrow(new SOPGPException.ExpectedText()); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertExpectedText(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java index 50a8043..3c9724f 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java @@ -10,6 +10,11 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput; +import static sop.testsuite.assertions.SopExecutionAssertions.assertNoSignature; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption; import java.io.ByteArrayOutputStream; import java.io.File; @@ -21,7 +26,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -76,60 +80,75 @@ public class VerifyCmdTest { @Test public void notAfter_passedDown() throws SOPGPException.UnsupportedOption, ParseException { 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()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-after", "2019-10-29T18:36:45Z", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notAfter(date); } @Test public void notAfter_now() throws SOPGPException.UnsupportedOption { Date now = new Date(); - SopCLI.main(new String[] {"verify", "--not-after", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-after", "now", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notAfter(dateMatcher(now)); } @Test public void notAfter_dashCountsAsEndOfTime() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"verify", "--not-after", "-", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-after", "-", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notAfter(AbstractSopCmd.END_OF_TIME); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { 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()}); + assertUnsupportedOption(() -> + SopCLI.execute("verify", "--not-after", "2019-10-29T18:36:45Z", + signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test public void notBefore_passedDown() throws SOPGPException.UnsupportedOption, ParseException { 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()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-before", "2019-10-29T18:36:45Z", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notBefore(date); } @Test public void notBefore_now() throws SOPGPException.UnsupportedOption { Date now = new Date(); - SopCLI.main(new String[] {"verify", "--not-before", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-before", "now", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notBefore(dateMatcher(now)); } @Test public void notBefore_dashCountsAsBeginningOfTime() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"verify", "--not-before", "-", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-before", "-", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { 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()}); + assertUnsupportedOption(() -> + SopCLI.execute("verify", "--not-before", "2019-10-29T18:36:45Z", + signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test public void notBeforeAndNotAfterAreCalledWithDefaultValues() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notAfter(dateMatcher(new Date())); verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME); } @@ -139,43 +158,43 @@ public class VerifyCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void cert_fileNotFoundCausesExit61() { - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), "invalid.asc"}); + assertMissingInput(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), "invalid.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void cert_badDataCausesExit41() throws SOPGPException.BadData, IOException { when(detachedVerify.cert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void signature_fileNotFoundCausesExit61() { - SopCLI.main(new String[] {"verify", "invalid.sig", cert.getAbsolutePath()}); + assertMissingInput(() -> + SopCLI.execute("verify", "invalid.sig", cert.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void signature_badDataCausesExit41() throws SOPGPException.BadData, IOException { when(detachedVerify.signatures((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.NoSignature.EXIT_CODE) public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.NoSignature()); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertNoSignature(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test @@ -192,7 +211,8 @@ public class VerifyCmdTest { ByteArrayOutputStream out = new ByteArrayOutputStream(); System.setOut(new PrintStream(out)); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); System.setOut(originalSout); diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java index e284e35..92850bd 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java @@ -4,19 +4,19 @@ package sop.cli.picocli.commands; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.SOP; import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; import sop.operation.Version; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption; + public class VersionCmdTest { private Version version; @@ -29,6 +29,8 @@ public class VersionCmdTest { when(version.getVersion()).thenReturn("1.0"); when(version.getExtendedVersion()).thenReturn("MockSop Extended Version Information"); when(version.getBackendVersion()).thenReturn("Foo"); + when(version.getSopSpecVersion()).thenReturn("draft-dkg-openpgp-stateless-cli-XX"); + when(version.getSopVVersion()).thenReturn("1.0"); when(sop.version()).thenReturn(version); SopCLI.setSopInstance(sop); @@ -36,26 +38,41 @@ public class VersionCmdTest { @Test public void assertVersionCommandWorks() { - SopCLI.main(new String[] {"version"}); + assertSuccess(() -> + SopCLI.execute("version")); verify(version, times(1)).getVersion(); verify(version, times(1)).getName(); } @Test public void assertExtendedVersionCommandWorks() { - SopCLI.main(new String[] {"version", "--extended"}); + assertSuccess(() -> + SopCLI.execute("version", "--extended")); verify(version, times(1)).getExtendedVersion(); } @Test public void assertBackendVersionCommandWorks() { - SopCLI.main(new String[] {"version", "--backend"}); + assertSuccess(() -> + SopCLI.execute("version", "--backend")); verify(version, times(1)).getBackendVersion(); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) + public void assertSpecVersionCommandWorks() { + assertSuccess(() -> + SopCLI.execute("version", "--sop-spec")); + } + + @Test + public void assertSOPVVersionCommandWorks() { + assertSuccess(() -> + SopCLI.execute("version", "--sopv")); + } + + @Test public void assertInvalidOptionResultsInExit37() { - SopCLI.main(new String[] {"version", "--invalid"}); + assertUnsupportedOption(() -> + SopCLI.execute("version", "--invalid")); } } diff --git a/sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java b/sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java new file mode 100644 index 0000000..bd07f0b --- /dev/null +++ b/sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.assertions; + +import sop.exception.SOPGPException; + +import java.util.function.IntSupplier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +/** + * DSL for testing the return values of SOP method calls. + */ +public class SopExecutionAssertions { + + /** + * Assert that the execution of the given function returns 0. + * + * @param function function to execute + */ + public static void assertSuccess(IntSupplier function) { + assertEquals(0, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns a generic error with error code 1. + * + * @param function function to execute. + */ + public static void assertGenericError(IntSupplier function) { + assertEquals(1, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns a non-zero error code. + * + * @param function function to execute + */ + public static void assertAnyError(IntSupplier function) { + assertNotEquals(0, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 3 + * (which corresponds to {@link sop.exception.SOPGPException.NoSignature}). + * + * @param function function to execute. + */ + public static void assertNoSignature(IntSupplier function) { + assertEquals(SOPGPException.NoSignature.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 13 + * (which corresponds to {@link sop.exception.SOPGPException.UnsupportedAsymmetricAlgo}). + * + * @param function function to execute. + */ + public static void assertUnsupportedAsymmetricAlgo(IntSupplier function) { + assertEquals(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 17 + * (which corresponds to {@link sop.exception.SOPGPException.CertCannotEncrypt}). + * + * @param function function to execute. + */ + public static void assertCertCannotEncrypt(IntSupplier function) { + assertEquals(SOPGPException.CertCannotEncrypt.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 19 + * (which corresponds to {@link sop.exception.SOPGPException.MissingArg}). + * + * @param function function to execute. + */ + public static void assertMissingArg(IntSupplier function) { + assertEquals(SOPGPException.MissingArg.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 23 + * (which corresponds to {@link sop.exception.SOPGPException.IncompleteVerification}). + * + * @param function function to execute. + */ + public static void assertIncompleteVerification(IntSupplier function) { + assertEquals(SOPGPException.IncompleteVerification.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 29 + * (which corresponds to {@link sop.exception.SOPGPException.CannotDecrypt}). + * + * @param function function to execute. + */ + public static void assertCannotDecrypt(IntSupplier function) { + assertEquals(SOPGPException.CannotDecrypt.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 31 + * (which corresponds to {@link sop.exception.SOPGPException.PasswordNotHumanReadable}). + * + * @param function function to execute. + */ + public static void assertPasswordNotHumanReadable(IntSupplier function) { + assertEquals(SOPGPException.PasswordNotHumanReadable.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 37 + * (which corresponds to {@link sop.exception.SOPGPException.UnsupportedOption}). + * + * @param function function to execute. + */ + public static void assertUnsupportedOption(IntSupplier function) { + assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 41 + * (which corresponds to {@link sop.exception.SOPGPException.BadData}). + * + * @param function function to execute. + */ + public static void assertBadData(IntSupplier function) { + assertEquals(SOPGPException.BadData.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 53 + * (which corresponds to {@link sop.exception.SOPGPException.ExpectedText}). + * + * @param function function to execute. + */ + public static void assertExpectedText(IntSupplier function) { + assertEquals(SOPGPException.ExpectedText.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 59 + * (which corresponds to {@link sop.exception.SOPGPException.OutputExists}). + * + * @param function function to execute. + */ + public static void assertOutputExists(IntSupplier function) { + assertEquals(SOPGPException.OutputExists.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 61 + * (which corresponds to {@link sop.exception.SOPGPException.MissingInput}). + * + * @param function function to execute. + */ + public static void assertMissingInput(IntSupplier function) { + assertEquals(SOPGPException.MissingInput.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 67 + * (which corresponds to {@link sop.exception.SOPGPException.KeyIsProtected}). + * + * @param function function to execute. + */ + public static void assertKeyIsProtected(IntSupplier function) { + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 69 + * (which corresponds to {@link sop.exception.SOPGPException.UnsupportedSubcommand}). + * + * @param function function to execute. + */ + public static void assertUnsupportedSubcommand(IntSupplier function) { + assertEquals(SOPGPException.UnsupportedSubcommand.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 71 + * (which corresponds to {@link sop.exception.SOPGPException.UnsupportedSpecialPrefix}). + * + * @param function function to execute. + */ + public static void assertUnsupportedSpecialPrefix(IntSupplier function) { + assertEquals(SOPGPException.UnsupportedSpecialPrefix.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 73 + * (which corresponds to {@link sop.exception.SOPGPException.AmbiguousInput}). + * + * @param function function to execute. + */ + public static void assertAmbiguousInput(IntSupplier function) { + assertEquals(SOPGPException.AmbiguousInput.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 79 + * (which corresponds to {@link sop.exception.SOPGPException.KeyCannotSign}). + * + * @param function function to execute. + */ + public static void assertKeyCannotSign(IntSupplier function) { + assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 83 + * (which corresponds to {@link sop.exception.SOPGPException.IncompatibleOptions}). + * + * @param function function to execute. + */ + public static void assertIncompatibleOptions(IntSupplier function) { + assertEquals(SOPGPException.IncompatibleOptions.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 89 + * (which corresponds to {@link sop.exception.SOPGPException.UnsupportedProfile}). + * + * @param function function to execute. + */ + public static void assertUnsupportedProfile(IntSupplier function) { + assertEquals(SOPGPException.UnsupportedProfile.EXIT_CODE, function.getAsInt()); + } +} From bb026bcbebcffeda0333333f3725f00e769e24f5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 27 Mar 2024 21:57:04 +0100 Subject: [PATCH 069/110] Mark ProxyOutputStream as deprecated --- sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt index da6c4fa..a608c89 100644 --- a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt +++ b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt @@ -15,6 +15,7 @@ import java.io.OutputStream * class is useful if we need to provide an [OutputStream] at one point in time when the final * target output stream is not yet known. */ +@Deprecated("Marked for removal.") class ProxyOutputStream : OutputStream() { private val buffer = ByteArrayOutputStream() private var swapped: OutputStream? = null From 547acdb740e5dae2f24dca06795794ea9dc7c640 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 30 Mar 2024 19:00:09 +0100 Subject: [PATCH 070/110] Remove label() option from armor() operation --- .../kotlin/sop/external/operation/ArmorExternal.kt | 3 --- .../src/main/resources/msg_armor.properties | 1 - .../src/main/resources/msg_armor_de.properties | 1 - sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt | 14 -------------- sop-java/src/main/kotlin/sop/operation/Armor.kt | 12 ------------ 5 files changed, 31 deletions(-) delete mode 100644 sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt diff --git a/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt index f80c57b..b202746 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt @@ -7,7 +7,6 @@ package sop.external.operation import java.io.InputStream import java.util.Properties import sop.Ready -import sop.enums.ArmorLabel import sop.exception.SOPGPException import sop.external.ExternalSOP import sop.operation.Armor @@ -18,8 +17,6 @@ class ArmorExternal(binary: String, environment: Properties) : Armor { private val commandList: MutableList = mutableListOf(binary, "armor") private val envList: List = ExternalSOP.propertiesToEnv(environment) - override fun label(label: ArmorLabel): Armor = apply { commandList.add("--label=$label") } - @Throws(SOPGPException.BadData::class) override fun data(data: InputStream): Ready = ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) diff --git a/sop-java-picocli/src/main/resources/msg_armor.properties b/sop-java-picocli/src/main/resources/msg_armor.properties index 2f4e217..b4dcb59 100644 --- a/sop-java-picocli/src/main/resources/msg_armor.properties +++ b/sop-java-picocli/src/main/resources/msg_armor.properties @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: Apache-2.0 usage.header=Add ASCII Armor to standard input -label=Label to be used in the header and tail of the armoring stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 diff --git a/sop-java-picocli/src/main/resources/msg_armor_de.properties b/sop-java-picocli/src/main/resources/msg_armor_de.properties index a2303e9..4c365a8 100644 --- a/sop-java-picocli/src/main/resources/msg_armor_de.properties +++ b/sop-java-picocli/src/main/resources/msg_armor_de.properties @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: Apache-2.0 usage.header=Schütze Standard-Eingabe mit ASCII Armor -label=Label für Kopf- und Fußzeile der ASCII Armor stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 diff --git a/sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt b/sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt deleted file mode 100644 index 8b4e2cd..0000000 --- a/sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums - -@Deprecated("Use of armor labels is deprecated.") -enum class ArmorLabel { - auto, - sig, - key, - cert, - message -} diff --git a/sop-java/src/main/kotlin/sop/operation/Armor.kt b/sop-java/src/main/kotlin/sop/operation/Armor.kt index e89708b..be7f1a3 100644 --- a/sop-java/src/main/kotlin/sop/operation/Armor.kt +++ b/sop-java/src/main/kotlin/sop/operation/Armor.kt @@ -7,22 +7,10 @@ package sop.operation import java.io.IOException import java.io.InputStream import sop.Ready -import sop.enums.ArmorLabel import sop.exception.SOPGPException.BadData -import sop.exception.SOPGPException.UnsupportedOption interface Armor { - /** - * Overrides automatic detection of label. - * - * @param label armor label - * @return builder instance - */ - @Deprecated("Use of armor labels is deprecated and will be removed in a future release.") - @Throws(UnsupportedOption::class) - fun label(label: ArmorLabel): Armor - /** * Armor the provided data. * From eadea08d3c6cdfb67b0d9699e00e1f41df7f5851 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 9 Jul 2024 14:29:22 +0200 Subject: [PATCH 071/110] Add new SOPGPException types related to hardware modules --- .../kotlin/sop/exception/SOPGPException.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt index 2473258..bc9131f 100644 --- a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt +++ b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt @@ -305,4 +305,36 @@ abstract class SOPGPException : RuntimeException { const val EXIT_CODE = 89 } } + + /** + * The sop implementation supports some form of hardware-backed secret keys, but could not + * identify the hardware device. + */ + class NoHardwareKeyFound : SOPGPException { + constructor() : super() + + constructor(errorMsg: String) : super(errorMsg) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 97 + } + } + + /** + * The sop implementation tried to use a hardware-backed secret key, but the cryptographic + * hardware refused the operation for some reason other than a bad PIN or password. + */ + class HardwareKeyFailure : SOPGPException { + constructor() : super() + + constructor(errorMsg: String) : super(errorMsg) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 101 + } + } } From a8a753536a3e438b6fc8cd4a4fa2d99d646cdcb0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 9 Jul 2024 14:39:03 +0200 Subject: [PATCH 072/110] Add translations for new hardware exception error messages --- sop-java-picocli/src/main/resources/msg_sop.properties | 2 ++ sop-java-picocli/src/main/resources/msg_sop_de.properties | 2 ++ 2 files changed, 4 insertions(+) diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 7979eb3..94e4dc0 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -36,6 +36,8 @@ usage.exitCodeList.17=73:Ambiguous input (a filename matching the designator alr usage.exitCodeList.18=79:Key is not signing capable usage.exitCodeList.19=83:Options were supplied that are incompatible with each other usage.exitCodeList.20=89:The requested profile is unsupported, or the indicated subcommand does not accept profiles +usage.exitCodeList.21=97:The implementation supports some form of hardware-backed secret keys, but could not identify the hardware device +usage.exitCodeList.22=101:The implementation tried to use a hardware-backed secret key, but the cryptographic hardware refused the operation for some reason other than a bad PIN or password ## SHARED RESOURCES stacktrace=Print stacktrace diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 40a316d..786fa36 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -36,6 +36,8 @@ usage.exitCodeList.17=73:Mehrdeutige Eingabe (ein Dateiname, der dem Bezeichner usage.exitCodeList.18=79:Schlüssel ist nicht fähig zu signieren usage.exitCodeList.19=83:Miteinander inkompatible Optionen spezifiziert usage.exitCodeList.20=89:Das angeforderte Profil wird nicht unterstützt, oder der angegebene Unterbefehl akzeptiert keine Profile +usage.exitCodeList.21=97:Die Anwendung unterstützt hardwaregestützte private Schlüssel, aber kann das Gerät nicht identifizieren +usage.exitCodeList.22=101:Die Anwendung versuchte, einen hardwaregestützten Schlüssel zu verwenden, aber das Gerät lehnte den Vorgang aus einem anderen Grund als einer falschen PIN oder einem falschen Passwort ab ## SHARED RESOURCES stacktrace=Stacktrace ausgeben From 1fd316185184f3981f16741040d64a18ccafd6f2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 19:20:33 +0200 Subject: [PATCH 073/110] Properly match MissingArg exception code --- .../main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt index 29aa77b..5778bb9 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt @@ -21,6 +21,8 @@ class SOPExceptionExitCodeMapper : IExitCodeExceptionMapper { // Unmatched subcommand SOPGPException.UnsupportedSubcommand.EXIT_CODE } + } else if (exception is MissingParameterException) { + SOPGPException.MissingArg.EXIT_CODE } else if (exception is ParameterException) { // Invalid option (e.g. `--as invalid`) SOPGPException.UnsupportedOption.EXIT_CODE From 471947ef9ccbf1a7cce8056c0367adff7e1b2c19 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 19:38:59 +0200 Subject: [PATCH 074/110] Fix woodpecker warnings --- .woodpecker/build.yml | 2 ++ .woodpecker/reuse.yml | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index ff59c4e..fab075a 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -1,5 +1,7 @@ steps: run: + when: + event: push image: gradle:7.6-jdk11-jammy commands: # Install Sequoia-SOP diff --git a/.woodpecker/reuse.yml b/.woodpecker/reuse.yml index d78c61e..b278a39 100644 --- a/.woodpecker/reuse.yml +++ b/.woodpecker/reuse.yml @@ -2,6 +2,8 @@ # See https://reuse.software/ steps: reuse: + when: + event: push image: fsfe/reuse:latest commands: - - reuse lint \ No newline at end of file + - reuse lint From 594b9029b252af8f322852b4219534f7c042a056 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 16:33:31 +0200 Subject: [PATCH 075/110] Document logback spam --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index e8bcbef..37c5577 100644 --- a/version.gradle +++ b/version.gradle @@ -12,7 +12,7 @@ allprojects { jsrVersion = '3.0.2' junitVersion = '5.8.2' junitSysExitVersion = '1.1.2' - logbackVersion = '1.2.13' + logbackVersion = '1.2.13' // 1.4+ cause CLI spam mockitoVersion = '4.5.1' picocliVersion = '4.6.3' slf4jVersion = '1.7.36' From 4eb6d1fdcb418b2bb02e86f9818fb4faa623cc04 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 12 Dec 2024 20:39:54 +0100 Subject: [PATCH 076/110] Prevent unmatched parameters when setting locale --- sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 1d5d46b..4ec3d3f 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -74,7 +74,9 @@ class SopCLI { @JvmStatic fun execute(vararg args: String): Int { // Set locale - CommandLine(InitLocale()).parseArgs(*args) + CommandLine(InitLocale()) + .setUnmatchedArgumentsAllowed(true) + .parseArgs(*args) // Re-set bundle with updated locale cliMsg = ResourceBundle.getBundle("msg_sop") From ca65cbe668c79f066c9a19234fa734ad4e234ff6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 12 Dec 2024 20:40:19 +0100 Subject: [PATCH 077/110] For now, do not re-set msg bundle (graal) --- sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 4ec3d3f..fcc7e74 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -83,8 +83,6 @@ class SopCLI { return CommandLine(SopCLI::class.java) .apply { - // explicitly set help command resource bundle - subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help")) // Hide generate-completion command subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true) // overwrite executable name From b3b8da4e358fa02b2009a073a2c4a00bbc3a48ab Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Dec 2024 16:41:01 +0100 Subject: [PATCH 078/110] Move testfixtures to own artifact --- external-sop/build.gradle | 2 +- settings.gradle | 1 + sop-java-picocli/build.gradle | 2 +- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 4 +--- sop-java-testfixtures/build.gradle | 24 +++++++++++++++++++ .../src/main}/java/sop/testsuite/JUtils.java | 0 .../sop/testsuite/SOPInstanceFactory.java | 0 .../main}/java/sop/testsuite/TestData.java | 0 .../assertions/SopExecutionAssertions.java | 0 .../assertions/VerificationAssert.java | 0 .../assertions/VerificationListAssert.java | 0 .../testsuite/assertions/package-info.java | 0 .../testsuite/operation/AbstractSOPTest.java | 0 .../testsuite/operation/ArmorDearmorTest.java | 0 .../operation/ChangeKeyPasswordTest.java | 0 .../operation/DecryptWithSessionKeyTest.java | 0 .../DetachedSignDetachedVerifyTest.java | 0 .../operation/EncryptDecryptTest.java | 0 .../testsuite/operation/ExtractCertTest.java | 0 .../testsuite/operation/GenerateKeyTest.java | 0 ...ineSignInlineDetachDetachedVerifyTest.java | 0 .../operation/InlineSignInlineVerifyTest.java | 0 .../testsuite/operation/ListProfilesTest.java | 0 .../testsuite/operation/RevokeKeyTest.java | 0 .../sop/testsuite/operation/VersionTest.java | 0 .../sop/testsuite/operation/package-info.java | 0 .../java/sop/testsuite/package-info.java | 0 .../sop/testsuite/AbortOnUnsupportedOption.kt | 0 .../AbortOnUnsupportedOptionExtension.kt | 0 sop-java/build.gradle | 4 +--- 30 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 sop-java-testfixtures/build.gradle rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/JUtils.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/SOPInstanceFactory.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/TestData.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/assertions/SopExecutionAssertions.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/assertions/VerificationAssert.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/assertions/VerificationListAssert.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/assertions/package-info.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/AbstractSOPTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/ArmorDearmorTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/ChangeKeyPasswordTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/EncryptDecryptTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/ExtractCertTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/GenerateKeyTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/ListProfilesTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/RevokeKeyTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/VersionTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/package-info.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/package-info.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt (100%) diff --git a/external-sop/build.gradle b/external-sop/build.gradle index 1bb86fc..d1a7ffb 100644 --- a/external-sop/build.gradle +++ b/external-sop/build.gradle @@ -27,7 +27,7 @@ dependencies { // The ExternalTestSubjectFactory reads json config file to find configured SOP binaries... testImplementation "com.google.code.gson:gson:$gsonVersion" // ...and extends TestSubjectFactory - testImplementation(testFixtures(project(":sop-java"))) + testImplementation(project(":sop-java-testfixtures")) } test { diff --git a/settings.gradle b/settings.gradle index 5dc6372..1cb66be 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,5 +6,6 @@ rootProject.name = 'SOP-Java' include 'sop-java', 'sop-java-picocli', + 'sop-java-testfixtures', 'external-sop' diff --git a/sop-java-picocli/build.gradle b/sop-java-picocli/build.gradle index 0596ad3..dbf0cc1 100644 --- a/sop-java-picocli/build.gradle +++ b/sop-java-picocli/build.gradle @@ -17,7 +17,7 @@ dependencies { // SOP implementation(project(":sop-java")) - testImplementation(testFixtures(project(":sop-java"))) + testImplementation(project(":sop-java-testfixtures")) // CLI implementation "info.picocli:picocli:$picocliVersion" diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index fcc7e74..b919370 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -74,9 +74,7 @@ class SopCLI { @JvmStatic fun execute(vararg args: String): Int { // Set locale - CommandLine(InitLocale()) - .setUnmatchedArgumentsAllowed(true) - .parseArgs(*args) + CommandLine(InitLocale()).setUnmatchedArgumentsAllowed(true).parseArgs(*args) // Re-set bundle with updated locale cliMsg = ResourceBundle.getBundle("msg_sop") diff --git a/sop-java-testfixtures/build.gradle b/sop-java-testfixtures/build.gradle new file mode 100644 index 0000000..d3d4a1e --- /dev/null +++ b/sop-java-testfixtures/build.gradle @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +plugins { + id 'java-library' +} + +group 'org.pgpainless' + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":sop-java")) + implementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + implementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" + runtimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + // @Nullable, @Nonnull annotations + implementation "com.google.code.findbugs:jsr305:3.0.2" + +} diff --git a/sop-java/src/testFixtures/java/sop/testsuite/JUtils.java b/sop-java-testfixtures/src/main/java/sop/testsuite/JUtils.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/JUtils.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/JUtils.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/SOPInstanceFactory.java b/sop-java-testfixtures/src/main/java/sop/testsuite/SOPInstanceFactory.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/SOPInstanceFactory.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/SOPInstanceFactory.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/TestData.java b/sop-java-testfixtures/src/main/java/sop/testsuite/TestData.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/TestData.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/TestData.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/SopExecutionAssertions.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/assertions/SopExecutionAssertions.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationAssert.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationAssert.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationListAssert.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationListAssert.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationListAssert.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationListAssert.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/assertions/package-info.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/package-info.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/assertions/package-info.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/assertions/package-info.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/ArmorDearmorTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/ArmorDearmorTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/ExtractCertTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/ExtractCertTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/GenerateKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/GenerateKeyTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/package-info.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/package-info.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/package-info.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/package-info.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/package-info.java b/sop-java-testfixtures/src/main/java/sop/testsuite/package-info.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/package-info.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/package-info.java diff --git a/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt b/sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt similarity index 100% rename from sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt rename to sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt diff --git a/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt b/sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt similarity index 100% rename from sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt rename to sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt diff --git a/sop-java/build.gradle b/sop-java/build.gradle index ca546bf..a235a35 100644 --- a/sop-java/build.gradle +++ b/sop-java/build.gradle @@ -4,7 +4,6 @@ plugins { id 'java-library' - id 'java-test-fixtures' } group 'org.pgpainless' @@ -17,8 +16,7 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - testFixturesImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" - testFixturesImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" + testImplementation(project(":sop-java-testfixtures")) // @Nullable, @Nonnull annotations implementation "com.google.code.findbugs:jsr305:3.0.2" From b1e1a2283f82ab63c393b6c2f5efa6b5ffc0b676 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 15 Dec 2024 18:19:30 +0100 Subject: [PATCH 079/110] Update changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index feee975..367064c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 10.1.0-SNAPSHOT +- `sop-java`: + - Remove `label()` option from `armor()` subcommand + - Move test-fixtures artifact built with the `testFixtures` plugin into + its own module `sop-java-testfixtures`, which can be consumed by maven builds. +- `sop-java-picocli`: + - Properly map `MissingParameterException` to `MissingArg` exit code + - As a workaround for native builds using graalvm: + - Do not re-set message bundles dynamically (fails in native builds) + - Prevent an unmatched argument error + ## 10.0.3 - CLI `change-key-password`: Fix indirect parameter passing for new and old passwords (thanks to @dkg for the report) - Backport: `revoke-key`: Allow for multiple password options From 84e381fe8eac974547cdd4f39804d9a9a24b1b48 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 21 Dec 2024 14:49:45 +0100 Subject: [PATCH 080/110] Write sop-java version to sop-java-version.properties --- sop-java/build.gradle | 9 +++++++++ sop-java/src/main/resources/sop-java-version.properties | 1 + 2 files changed, 10 insertions(+) create mode 100644 sop-java/src/main/resources/sop-java-version.properties diff --git a/sop-java/build.gradle b/sop-java/build.gradle index a235a35..c6f4e4e 100644 --- a/sop-java/build.gradle +++ b/sop-java/build.gradle @@ -1,7 +1,10 @@ +import org.apache.tools.ant.filters.ReplaceTokens + // SPDX-FileCopyrightText: 2021 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 +import org.apache.tools.ant.filters.* plugins { id 'java-library' } @@ -23,6 +26,12 @@ dependencies { } +processResources { + filter ReplaceTokens, tokens: [ + "project.version": project.version.toString() + ] +} + test { useJUnitPlatform() } diff --git a/sop-java/src/main/resources/sop-java-version.properties b/sop-java/src/main/resources/sop-java-version.properties new file mode 100644 index 0000000..5f6f682 --- /dev/null +++ b/sop-java/src/main/resources/sop-java-version.properties @@ -0,0 +1 @@ +sop-java-version=@project.version@ \ No newline at end of file From 2b6015f59ad00fb62f35da74cfa839c23973ab08 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 28 Jan 2025 12:01:15 +0100 Subject: [PATCH 081/110] Add license header to properties files --- sop-java/src/main/resources/sop-java-version.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sop-java/src/main/resources/sop-java-version.properties b/sop-java/src/main/resources/sop-java-version.properties index 5f6f682..a2f509b 100644 --- a/sop-java/src/main/resources/sop-java-version.properties +++ b/sop-java/src/main/resources/sop-java-version.properties @@ -1 +1,4 @@ +# SPDX-FileCopyrightText: 2025 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 sop-java-version=@project.version@ \ No newline at end of file From f92a73a5ad46eb104929055e62317e088c3c0b38 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 28 Jan 2025 12:01:46 +0100 Subject: [PATCH 082/110] Add back legacy --verify-out option alias for decrypt cmd --- .../src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt index 3f15f07..de98f17 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt @@ -29,7 +29,7 @@ class DecryptCmd : AbstractSopCmd() { @Option(names = [OPT_WITH_PASSWORD], paramLabel = "PASSWORD") var withPassword: List = listOf() - @Option(names = [OPT_VERIFICATIONS_OUT], paramLabel = "VERIFICATIONS") + @Option(names = [OPT_VERIFICATIONS_OUT, "--verify-out"], paramLabel = "VERIFICATIONS") var verifyOut: String? = null @Option(names = [OPT_VERIFY_WITH], paramLabel = "CERT") var certs: List = listOf() From 9ec3cc911bf1d69593231b1a1221362bec21297c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 28 Jan 2025 12:12:04 +0100 Subject: [PATCH 083/110] Add bcsop reference in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index baeb874..6be51d0 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ allowing to delegate the implementation logic to an arbitrary SOP CLI implementa |-------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------| | [pgpainless-sop](https://github.com/pgpainless/pgpainless/tree/main/pgpainless-sop) | Implementation of `sop-java` using PGPainless | | [external-sop](https://github.com/pgpainless/sop-java/tree/main/external-sop) | Implementation of `sop-java` that allows binding to external SOP binaries such as `sqop` | +| [bcsop](https://codeberg.org/PGPainless/bc-sop) | Implementation of `sop-java` using vanilla Bouncy Castle | ### Implementations in other languages | Project | Language | From 690ba6dc16efa6ca60029620af3af0f28ac7efcf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 28 Jan 2025 12:12:17 +0100 Subject: [PATCH 084/110] Add rpgpie-sop reference in README --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6be51d0..e6ee9ab 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,8 @@ allowing to delegate the implementation logic to an arbitrary SOP CLI implementa | [bcsop](https://codeberg.org/PGPainless/bc-sop) | Implementation of `sop-java` using vanilla Bouncy Castle | ### Implementations in other languages -| Project | Language | -|-------------------------------------------------|----------| -| [sop-rs](https://sequoia-pgp.gitlab.io/sop-rs/) | Rust | -| [SOP for python](https://pypi.org/project/sop/) | Python | +| Project | Language | +|---------------------------------------------------|----------| +| [sop-rs](https://sequoia-pgp.gitlab.io/sop-rs/) | Rust | +| [SOP for python](https://pypi.org/project/sop/) | Python | +| [rpgpie-sop](https://crates.io/crates/rpgpie-sop) | Rust | From 97e91f50ab924dc42533491751a57e2640df62d1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 28 Jan 2025 12:15:30 +0100 Subject: [PATCH 085/110] Migrate pipeline definition to use from_secret https://woodpecker-ci.org/docs/usage/secrets\#use-secrets-in-settings-and-environment --- .woodpecker/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index fab075a..2138cb4 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -16,4 +16,6 @@ steps: - gradle check javadocAll # Code has coverage - gradle jacocoRootReport coveralls - secrets: [coveralls_repo_token] + environment: + coveralls_repo_token: + from_secret: coveralls_repo_token From f2602bb413ba8b6e4f3edded7226814392252e25 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 21:04:50 +0100 Subject: [PATCH 086/110] Bump version to 10.1.0-SNAPSHOT --- .../src/main/kotlin/sop/operation/Version.kt | 16 ++++++++++++++++ version.gradle | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/sop-java/src/main/kotlin/sop/operation/Version.kt b/sop-java/src/main/kotlin/sop/operation/Version.kt index 5f26491..a10fe7c 100644 --- a/sop-java/src/main/kotlin/sop/operation/Version.kt +++ b/sop-java/src/main/kotlin/sop/operation/Version.kt @@ -4,6 +4,9 @@ package sop.operation +import java.io.IOException +import java.io.InputStream +import java.util.* import kotlin.jvm.Throws import sop.exception.SOPGPException @@ -107,4 +110,17 @@ interface Version { * this method throws an [SOPGPException.UnsupportedOption] instead. */ @Throws(SOPGPException.UnsupportedOption::class) fun getSopVVersion(): String + + /** Return the current version of the SOP-Java library. */ + fun getSopJavaVersion(): String? { + return try { + val resourceIn: InputStream = + javaClass.getResourceAsStream("/sop-java-version.properties") + ?: throw IOException("File sop-java-version.properties not found.") + val properties = Properties().apply { load(resourceIn) } + properties.getProperty("sop-java-version") + } catch (e: IOException) { + null + } + } } diff --git a/version.gradle b/version.gradle index 37c5577..3b0de55 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '10.0.4' + shortVersion = '10.1.0' isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 From 924cfaa140b53c5652adacd0f2115bdd0a5b2544 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 21:18:24 +0100 Subject: [PATCH 087/110] Update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e6ee9ab..f2e207f 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ The repository contains the following modules: compatible with the SOP-CLI specification. * [external-sop](/external-sop) contains an API implementation that can be used to forward API calls to a SOP executable, allowing to delegate the implementation logic to an arbitrary SOP CLI implementation. +* [sop-java-testfixtures](/sop-java-testfixtures) contains a test suite that can be shared by downstream implementations + of `sop-java`. ## Known Implementations (Please expand!) From c145f8bb37967722f821e2516f09945ea7a34ef7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 21:19:41 +0100 Subject: [PATCH 088/110] SOP-Java 10.1.0 --- CHANGELOG.md | 2 +- version.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 367064c..49113f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 10.1.0-SNAPSHOT +## 10.1.0 - `sop-java`: - Remove `label()` option from `armor()` subcommand - Move test-fixtures artifact built with the `testFixtures` plugin into diff --git a/version.gradle b/version.gradle index 3b0de55..13c3b5f 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '10.1.0' - isSnapshot = true + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From d1893c5ea0ee239d3cef344922ce95c4d9fdb104 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 21:22:31 +0100 Subject: [PATCH 089/110] SOP-Java 10.1.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 13c3b5f..33a2251 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '10.1.0' - isSnapshot = false + shortVersion = '10.1.1' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 51ba24ddbeaa8d5642fe8ac1bf90ac3a19553779 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Apr 2025 14:18:46 +0200 Subject: [PATCH 090/110] Enable kapt annotation processing to properly embed picocli configuration files for native images into the -cli jar file For this it is apparently necessary to upgrade kotlin to 1.9.21 See https://stackoverflow.com/a/79030947/11150851 --- build.gradle | 3 ++- sop-java-picocli/build.gradle | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 577c2aa..78a7267 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { plugins { id 'ru.vyarus.animalsniffer' version '1.5.3' - id 'org.jetbrains.kotlin.jvm' version "1.8.10" + id 'org.jetbrains.kotlin.jvm' version "1.9.21" id 'com.diffplug.spotless' version '6.22.0' apply false } @@ -32,6 +32,7 @@ allprojects { apply plugin: 'jacoco' apply plugin: 'checkstyle' apply plugin: 'kotlin' + apply plugin: 'kotlin-kapt' apply plugin: 'com.diffplug.spotless' // For non-cli modules enable android api compatibility check diff --git a/sop-java-picocli/build.gradle b/sop-java-picocli/build.gradle index dbf0cc1..2203abe 100644 --- a/sop-java-picocli/build.gradle +++ b/sop-java-picocli/build.gradle @@ -21,7 +21,7 @@ dependencies { // CLI implementation "info.picocli:picocli:$picocliVersion" - annotationProcessor "info.picocli:picocli-codegen:$picocliVersion" + kapt "info.picocli:picocli-codegen:$picocliVersion" // @Nonnull, @Nullable... implementation "com.google.code.findbugs:jsr305:$jsrVersion" @@ -33,6 +33,10 @@ application { mainClass = mainClassName } +compileJava { + options.compilerArgs += ["-Aproject=${project.group}/${project.name}"] +} + jar { dependsOn(":sop-java:jar") duplicatesStrategy(DuplicatesStrategy.EXCLUDE) From 57e2f8391bed9935d4b4c469dbb2b2dafdb08ddb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Apr 2025 10:43:49 +0200 Subject: [PATCH 091/110] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49113f6..3028216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 10.1.1-SNAPSHOT +- Prepare jar files for use in native images, e.g. using GraalVM by generating and including + configuration files for reflection, resources and dynamic proxies. + ## 10.1.0 - `sop-java`: - Remove `label()` option from `armor()` subcommand From edb405d79e2321c921b61b8ee350004c24817301 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Apr 2025 12:11:20 +0200 Subject: [PATCH 092/110] Add TODO to remove ProxyOutputStream in 11.X --- sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt index a608c89..4ba24b8 100644 --- a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt +++ b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt @@ -16,6 +16,7 @@ import java.io.OutputStream * target output stream is not yet known. */ @Deprecated("Marked for removal.") +// TODO: Remove in 11.X class ProxyOutputStream : OutputStream() { private val buffer = ByteArrayOutputStream() private var swapped: OutputStream? = null From 859bb5bddecf9d13e7733da1f0eb9de0e6f8cafc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Apr 2025 12:16:00 +0200 Subject: [PATCH 093/110] Fix issues in kdoc --- .../main/kotlin/sop/external/operation/EncryptExternal.kt | 2 +- sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt | 2 +- sop-java/src/main/kotlin/sop/SigningResult.kt | 5 +++-- sop-java/src/main/kotlin/sop/operation/GenerateKey.kt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt index 12d9cff..679e09b 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt @@ -37,7 +37,7 @@ class EncryptExternal( override fun signWith(key: InputStream): Encrypt = apply { commandList.add("--sign-with=@ENV:SIGN_WITH_$argCounter") - envList.add("SIGN_WITH_$argCounter=${ExternalSOP.readString(key)}") + envList.add("SIGN_WITH_$argCounter=${readString(key)}") argCounter += 1 } diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index b919370..62065f4 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -60,7 +60,7 @@ class SopCLI { @JvmField var EXECUTABLE_NAME = "sop" @JvmField - @Option(names = ["--stacktrace"], scope = CommandLine.ScopeType.INHERIT) + @Option(names = ["--stacktrace"], scope = ScopeType.INHERIT) var stacktrace = false @JvmStatic diff --git a/sop-java/src/main/kotlin/sop/SigningResult.kt b/sop-java/src/main/kotlin/sop/SigningResult.kt index 29304ea..60888e0 100644 --- a/sop-java/src/main/kotlin/sop/SigningResult.kt +++ b/sop-java/src/main/kotlin/sop/SigningResult.kt @@ -9,8 +9,9 @@ package sop * * @param micAlg string identifying the digest mechanism used to create the signed message. This is * useful for setting the `micalg=` parameter for the multipart/signed content-type of a PGP/MIME - * object as described in section 5 of [RFC3156]. If more than one signature was generated and - * different digest mechanisms were used, the value of the micalg object is an empty string. + * object as described in section 5 of [RFC3156](https://www.rfc-editor.org/rfc/rfc3156#section-5). + * If more than one signature was generated and different digest mechanisms were used, the value + * of the micalg object is an empty string. */ data class SigningResult(val micAlg: MicAlg) { diff --git a/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt b/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt index 3b83b99..13de39a 100644 --- a/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt @@ -78,7 +78,7 @@ interface GenerateKey { fun signingOnly(): GenerateKey /** - * Generate the OpenPGP key and return it encoded as an [InputStream]. + * Generate the OpenPGP key and return it encoded as an [java.io.InputStream]. * * @return key * @throws MissingArg if no user-id was provided From dd07d1035019b9c520fa37d456fe4f8b512b25f8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Sep 2024 22:40:36 +0200 Subject: [PATCH 094/110] Add new exceptions --- .../src/main/resources/msg_sop.properties | 2 ++ .../src/main/resources/msg_sop_de.properties | 2 ++ .../kotlin/sop/exception/SOPGPException.kt | 30 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 94e4dc0..8179676 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -38,6 +38,8 @@ usage.exitCodeList.19=83:Options were supplied that are incompatible with each o usage.exitCodeList.20=89:The requested profile is unsupported, or the indicated subcommand does not accept profiles usage.exitCodeList.21=97:The implementation supports some form of hardware-backed secret keys, but could not identify the hardware device usage.exitCodeList.22=101:The implementation tried to use a hardware-backed secret key, but the cryptographic hardware refused the operation for some reason other than a bad PIN or password +usage.exitCodeList.23=103:The primary key of a KEYS object is too weak or revoked +usage.exitCodeList.24=107:The CERTS object has no matching User ID ## SHARED RESOURCES stacktrace=Print stacktrace diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 786fa36..0538cd9 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -38,6 +38,8 @@ usage.exitCodeList.19=83:Miteinander inkompatible Optionen spezifiziert usage.exitCodeList.20=89:Das angeforderte Profil wird nicht unterstützt, oder der angegebene Unterbefehl akzeptiert keine Profile usage.exitCodeList.21=97:Die Anwendung unterstützt hardwaregestützte private Schlüssel, aber kann das Gerät nicht identifizieren usage.exitCodeList.22=101:Die Anwendung versuchte, einen hardwaregestützten Schlüssel zu verwenden, aber das Gerät lehnte den Vorgang aus einem anderen Grund als einer falschen PIN oder einem falschen Passwort ab +usage.exitCodeList.23=103:Der primäre private Schlüssel ist zu schwach oder widerrufen +usage.exitCodeList.24=107:Das Zertifikat hat keine übereinstimmende User ID ## SHARED RESOURCES stacktrace=Stacktrace ausgeben diff --git a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt index bc9131f..1f9ce6b 100644 --- a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt +++ b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt @@ -337,4 +337,34 @@ abstract class SOPGPException : RuntimeException { const val EXIT_CODE = 101 } } + + /** + * The primary key of a KEYS object is too weak or revoked. + */ + class PrimaryKeyBad : SOPGPException { + constructor() : super() + + constructor(errorMsg: String) : super(errorMsg) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 103 + } + } + + /** + * The CERTS object has no matching User ID. + */ + class CertUserIdNoMatch : SOPGPException { + constructor() : super() + + constructor(errorMsg: String) : super(errorMsg) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 107 + } + } } From 023b4ea45bbee846b4aaac498686202adea61be1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Sep 2024 22:43:36 +0200 Subject: [PATCH 095/110] Add implementation of update-key command --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 1 + .../sop/cli/picocli/commands/UpdateKeyCmd.kt | 76 +++++++++++++++++++ .../main/resources/msg_update-key.properties | 19 +++++ .../resources/msg_update-key_de.properties | 18 +++++ sop-java/src/main/kotlin/sop/SOP.kt | 18 ++--- .../main/kotlin/sop/operation/UpdateKey.kt | 43 +++++++++++ 6 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt create mode 100644 sop-java-picocli/src/main/resources/msg_update-key.properties create mode 100644 sop-java-picocli/src/main/resources/msg_update-key_de.properties create mode 100644 sop-java/src/main/kotlin/sop/operation/UpdateKey.kt diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 62065f4..df8e883 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -27,6 +27,7 @@ import sop.exception.SOPGPException ChangeKeyPasswordCmd::class, RevokeKeyCmd::class, ExtractCertCmd::class, + UpdateKeyCmd::class, // Messaging subcommands SignCmd::class, VerifyCmd::class, diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt new file mode 100644 index 0000000..2afa015 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.* +import java.io.IOException + +@Command( + name = "update-key", + resourceBundle = "msg_update-key", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class UpdateKeyCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--signing-only"]) var signingOnly = false + + @Option(names = ["--no-new-mechanisms"]) var noNewMechanisms = false + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + @Option(names = ["--merge-certs"], paramLabel = "CERTS") + var mergeCerts: List = listOf() + + override fun run() { + val updateKey = throwIfUnsupportedSubcommand(SopCLI.getSop().updateKey(), "update-key") + + if (!armor) { + updateKey.noArmor() + } + + if (signingOnly) { + updateKey.signingOnly() + } + + if (noNewMechanisms) { + updateKey.noNewMechanisms() + } + + for (passwordFileName in withKeyPassword) { + try { + val password = stringFromInputStream(getInput(passwordFileName)) + updateKey.withKeyPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + for (certInput in mergeCerts) { + try { + getInput(certInput).use { certIn -> updateKey.mergeCerts(certIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput) + throw BadData(errorMsg, badData) + } + } + + try { + val ready = updateKey.key(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} \ No newline at end of file diff --git a/sop-java-picocli/src/main/resources/msg_update-key.properties b/sop-java-picocli/src/main/resources/msg_update-key.properties new file mode 100644 index 0000000..dd4446d --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_update-key.properties @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Keep a secret key up-to-date +no-armor=ASCII armor the output +signing-only=TODO: Document +no-new-mechanisms=Do not add feature support for new mechanisms, which the key did not previously support +with-key-password.0=Passphrase to unlock the secret key(s). +with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +merge-certs.0=Merge additional elements found in the corresponding CERTS objects into the updated secret keys +merge-certs.1=This can be used, for example, to absorb a third-party certification into the Transferable Secret Key + +stacktrace=Print stacktrace +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=%nParameters:%n +usage.synopsisHeading=Usage:\u0020 +usage.commandListHeading = %nCommands:%n +usage.optionListHeading = %nOptions:%n +usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_update-key_de.properties b/sop-java-picocli/src/main/resources/msg_update-key_de.properties new file mode 100644 index 0000000..86b999e --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_update-key_de.properties @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Halte einen Schlüssel auf dem neusten Stand +no-armor=Schütze Ausgabe mit ASCII Armor +signing-only=TODO: Dokumentieren +no-new-mechanisms=Füge keine neuen Funktionen hinzu, die der Schlüssel nicht bereits zuvor unterstützt hat +with-key-password.0=Passwort zum Entsperren der privaten Schlüssel +with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +merge-certs.0=Führe zusätzliche Elemente aus entsprechenden CERTS Objekten mit dem privaten Schlüssel zusammen +merge-certs.1=Dies kann zum Beispiel dazu genutzt werden, Zertifizierungen dritter in den privaten Schlüssel zu übernehmen + +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=%nParameter:%n +usage.synopsisHeading=Aufruf:\u0020 +usage.commandListHeading=%nBefehle:%n +usage.optionListHeading = %nOptionen:%n +usage.footerHeading=Powered by Picocli%n diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index 7fdd414..c53bb7d 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -4,18 +4,7 @@ package sop -import sop.operation.Armor -import sop.operation.ChangeKeyPassword -import sop.operation.Dearmor -import sop.operation.Decrypt -import sop.operation.DetachedSign -import sop.operation.Encrypt -import sop.operation.ExtractCert -import sop.operation.GenerateKey -import sop.operation.InlineDetach -import sop.operation.InlineSign -import sop.operation.ListProfiles -import sop.operation.RevokeKey +import sop.operation.* /** * Stateless OpenPGP Interface. This class provides a stateless interface to various OpenPGP related @@ -70,4 +59,9 @@ interface SOP : SOPV { /** Update a key's password. */ fun changeKeyPassword(): ChangeKeyPassword + + /** + * Keep a secret key up-to-date. + */ + fun updateKey(): UpdateKey } diff --git a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt new file mode 100644 index 0000000..1b12f6f --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import sop.Ready +import sop.exception.SOPGPException +import sop.util.UTF8Util +import java.io.IOException +import java.io.InputStream + +interface UpdateKey { + + /** + * Disable ASCII armor encoding of the output. + * + * @return builder instance + */ + fun noArmor(): UpdateKey + + @Throws(SOPGPException.UnsupportedOption::class) fun signingOnly(): UpdateKey + + @Throws(SOPGPException.UnsupportedOption::class) fun noNewMechanisms(): UpdateKey + + @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + fun withKeyPassword(password: String): UpdateKey = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + + @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + fun withKeyPassword(password: ByteArray): UpdateKey + + @Throws(SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) + fun mergeCerts(certs: InputStream): UpdateKey + + @Throws(SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) + fun mergeCerts(certs: ByteArray): UpdateKey = mergeCerts(certs.inputStream()) + + @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class, SOPGPException.PrimaryKeyBad::class) + fun key(key: InputStream): Ready + + @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class, SOPGPException.PrimaryKeyBad::class) + fun key(key: ByteArray): Ready = key(key.inputStream()) +} \ No newline at end of file From 3e6e47ed50a37fce94c833d2bbc2682627c9e645 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Sep 2024 22:43:50 +0200 Subject: [PATCH 096/110] Add implementation of merge-certs command --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 1 + .../sop/cli/picocli/commands/MergeCertsCmd.kt | 48 +++++++++++++++++++ .../main/resources/msg_merge-certs.properties | 15 ++++++ .../resources/msg_merge-certs_de.properties | 19 ++++++++ sop-java/src/main/kotlin/sop/SOP.kt | 5 ++ .../main/kotlin/sop/operation/MergeCerts.kt | 28 +++++++++++ 6 files changed, 116 insertions(+) create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt create mode 100644 sop-java-picocli/src/main/resources/msg_merge-certs.properties create mode 100644 sop-java-picocli/src/main/resources/msg_merge-certs_de.properties create mode 100644 sop-java/src/main/kotlin/sop/operation/MergeCerts.kt diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index df8e883..9d1a305 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -28,6 +28,7 @@ import sop.exception.SOPGPException RevokeKeyCmd::class, ExtractCertCmd::class, UpdateKeyCmd::class, + MergeCertsCmd::class, // Messaging subcommands SignCmd::class, VerifyCmd::class, diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt new file mode 100644 index 0000000..15b33f8 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import picocli.CommandLine +import picocli.CommandLine.Command +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import java.io.IOException + +@Command( + name = "merge-certs", + resourceBundle = "msg_merge-certs", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class MergeCertsCmd : AbstractSopCmd() { + + @CommandLine.Option(names = ["--no-armor"], negatable = true) + var armor = false + + @CommandLine.Parameters(paramLabel = "CERTS") + var updates: List = listOf() + + override fun run() { + val mergeCerts = throwIfUnsupportedSubcommand(SopCLI.getSop().mergeCerts(), "merge-certs") + + if (!armor) { + mergeCerts.noArmor() + } + + for (certFileName in updates) { + try { + getInput(certFileName).use { mergeCerts.updates(it) } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + try { + + val ready = mergeCerts.baseCertificates(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} \ No newline at end of file diff --git a/sop-java-picocli/src/main/resources/msg_merge-certs.properties b/sop-java-picocli/src/main/resources/msg_merge-certs.properties new file mode 100644 index 0000000..866db4b --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_merge-certs.properties @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.headerHeading=Merge OpenPGP certificates%n +usage.description=BLABLA +no-armor=ASCII armor the output +CERTS[0..*]=OpenPGP certificates from which updates shall be merged into the base certificates from standard input + +stacktrace=Print stacktrace +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=%nParameters:%n +usage.synopsisHeading=Usage:\u0020 +usage.commandListHeading = %nCommands:%n +usage.optionListHeading = %nOptions:%n +usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties b/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties new file mode 100644 index 0000000..021c535 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.headerHeading=OpenPGP Zertifikate zusammenführen%n%n +usage.header=Führe OpenPGP Zertifikate aus der Standardeingabe mit ensprechenden Elementen aus CERTS zusammen und gebe das Ergebnis auf der Standardausgabe aus +usage.description=Es werden nur Zertifikate auf die Standardausgabe geschrieben, welche Teil der Standardeingabe waren +no-armor=Schütze Ausgabe mit ASCII Armor +CERTS[0..*]=OpenPGP Zertifikate aus denen neue Elemente in die Basiszertifikate aus der Standardeingabe übernommen werden sollen + +usage.parameterList.0=STANDARDIN +usage.parameterList.1=STANDARDOUT + +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=%nParameter:%n +usage.synopsisHeading=Aufruf:\u0020 +usage.descriptionHeading=%nHinweise:%n +usage.commandListHeading=%nBefehle:%n +usage.optionListHeading = %nOptionen:%n +usage.footerHeading=Powered by Picocli%n diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index c53bb7d..1640d5f 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -64,4 +64,9 @@ interface SOP : SOPV { * Keep a secret key up-to-date. */ fun updateKey(): UpdateKey + + /** + * Merge OpenPGP certificates. + */ + fun mergeCerts(): MergeCerts } diff --git a/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt new file mode 100644 index 0000000..f60d291 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import sop.Ready +import sop.exception.SOPGPException +import java.io.IOException +import java.io.InputStream + +interface MergeCerts { + + @Throws(SOPGPException.UnsupportedOption::class) + fun noArmor(): MergeCerts + + @Throws(SOPGPException.BadData::class, IOException::class) + fun updates(updateCerts: InputStream): MergeCerts + + @Throws(SOPGPException.BadData::class, IOException::class) + fun updates(updateCerts: ByteArray): MergeCerts = updates(updateCerts.inputStream()) + + @Throws(SOPGPException.BadData::class, IOException::class) + fun baseCertificates(certs: InputStream): Ready + + @Throws(SOPGPException.BadData::class, IOException::class) + fun baseCertificates(certs: ByteArray): Ready = baseCertificates(certs.inputStream()) +} \ No newline at end of file From 5bebdb779a96fb35be9759d592bb68fa4064f1d1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 15:50:17 +0200 Subject: [PATCH 097/110] Add support for rendering help info for input and output --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 8 +- .../cli/picocli/commands/AbstractSopCmd.kt | 100 ++++++++++++++++++ .../src/main/resources/msg_sop.properties | 7 +- .../src/main/resources/msg_sop_de.properties | 5 +- 4 files changed, 115 insertions(+), 5 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 9d1a305..4db6b30 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -83,16 +83,20 @@ class SopCLI { return CommandLine(SopCLI::class.java) .apply { + // explicitly set help command resource bundle + subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help")) // Hide generate-completion command subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true) + // render Input/Output sections in help command + subcommands.values.filter { (it.getCommand() as Any) is AbstractSopCmd } // Only for AbstractSopCmd objects + .forEach { (it.getCommand() as AbstractSopCmd).installIORenderer(it) } // overwrite executable name commandName = EXECUTABLE_NAME // setup exception handling executionExceptionHandler = SOPExecutionExceptionHandler() exitCodeExceptionMapper = SOPExceptionExitCodeMapper() isCaseInsensitiveEnumValuesAllowed = true - } - .execute(*args) + }.execute(*args) } } diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt index 4629e57..65be1be 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt @@ -7,6 +7,11 @@ package sop.cli.picocli.commands import java.io.* import java.text.ParseException import java.util.* +import picocli.CommandLine +import picocli.CommandLine.Help +import picocli.CommandLine.Help.Column +import picocli.CommandLine.Help.TextTable +import picocli.CommandLine.IHelpSectionRenderer import sop.cli.picocli.commands.AbstractSopCmd.EnvironmentVariableResolver import sop.exception.SOPGPException.* import sop.util.UTCUtil.Companion.parseUTCDate @@ -215,11 +220,106 @@ abstract class AbstractSopCmd(locale: Locale = Locale.getDefault()) : Runnable { } } + /** + * See + * [Example](https://github.com/remkop/picocli/blob/main/picocli-examples/src/main/java/picocli/examples/customhelp/EnvironmentVariablesSection.java) + */ + class InputOutputHelpSectionRenderer(private val argument: Pair) : + IHelpSectionRenderer { + + override fun render(help: Help): String { + return argument.let { + val calcLen = + help.calcLongOptionColumnWidth( + help.commandSpec().options(), + help.commandSpec().positionalParameters(), + help.colorScheme()) + val keyLength = + help + .commandSpec() + .usageMessage() + .longOptionsMaxWidth() + .coerceAtMost(calcLen - 1) + val table = + TextTable.forColumns( + help.colorScheme(), + Column(keyLength + 7, 6, Column.Overflow.SPAN), + Column(width(help) - (keyLength + 7), 0, Column.Overflow.WRAP)) + table.setAdjustLineBreaksForWideCJKCharacters(adjustCJK(help)) + table.addRowValues("@|yellow ${argument.first}|@", argument.second ?: "") + table.toString() + } + } + + private fun adjustCJK(help: Help) = + help.commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters() + + private fun width(help: Help) = help.commandSpec().usageMessage().width() + } + + fun installIORenderer(cmd: CommandLine) { + val inputName = getResString(cmd, "standardInput") + if (inputName != null) { + cmd.helpSectionMap[SECTION_KEY_STANDARD_INPUT_HEADING] = IHelpSectionRenderer { + getResString(cmd, "standardInputHeading") + } + cmd.helpSectionMap[SECTION_KEY_STANDARD_INPUT_DETAILS] = + InputOutputHelpSectionRenderer( + inputName to getResString(cmd, "standardInputDescription")) + cmd.helpSectionKeys = + insertKey( + cmd.helpSectionKeys, + SECTION_KEY_STANDARD_INPUT_HEADING, + SECTION_KEY_STANDARD_INPUT_DETAILS) + } + + val outputName = getResString(cmd, "standardOutput") + if (outputName != null) { + cmd.helpSectionMap[SECTION_KEY_STANDARD_OUTPUT_HEADING] = IHelpSectionRenderer { + getResString(cmd, "standardOutputHeading") + } + cmd.helpSectionMap[SECTION_KEY_STANDARD_OUTPUT_DETAILS] = + InputOutputHelpSectionRenderer( + outputName to getResString(cmd, "standardOutputDescription")) + cmd.helpSectionKeys = + insertKey( + cmd.helpSectionKeys, + SECTION_KEY_STANDARD_OUTPUT_HEADING, + SECTION_KEY_STANDARD_OUTPUT_DETAILS) + } + } + + private fun insertKey(keys: List, header: String, details: String): List { + val index = + keys.indexOf(CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST_HEADING) + val result = keys.toMutableList() + result.add(index, header) + result.add(index + 1, details) + return result + } + + private fun getResString(cmd: CommandLine, key: String): String? = + try { + cmd.resourceBundle.getString(key) + } catch (m: MissingResourceException) { + try { + cmd.parent.resourceBundle.getString(key) + } catch (m: MissingResourceException) { + null + } + } + ?.let { String.format(it) } + companion object { const val PRFX_ENV = "@ENV:" const val PRFX_FD = "@FD:" + const val SECTION_KEY_STANDARD_INPUT_HEADING = "standardInputHeading" + const val SECTION_KEY_STANDARD_INPUT_DETAILS = "standardInput" + const val SECTION_KEY_STANDARD_OUTPUT_HEADING = "standardOutputHeading" + const val SECTION_KEY_STANDARD_OUTPUT_DETAILS = "standardOutput" + @JvmField val DAWN_OF_TIME = Date(0) @JvmField diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 8179676..d5d997a 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -9,10 +9,13 @@ locale=Locale for description texts # Generic usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n +standardInputHeading=%nInput:%n +standardOutputHeading=%nOutput:%n + # Exit Codes usage.exitCodeListHeading=%nExit Codes:%n usage.exitCodeList.0=\u00200:Successful program execution diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 0538cd9..73efe89 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -10,9 +10,12 @@ locale=Gebietsschema f # Generic usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n +standardInputHeading=%nEingabe:%n +standardOutputHeading=%nAusgabe:%n + # Exit Codes usage.exitCodeListHeading=%nExit Codes:%n usage.exitCodeList.0=\u00200:Erfolgreiche Programmausführung From 6fc849bb6aaadc3b40fec0e70021338ad4ea4e80 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 16:01:30 +0200 Subject: [PATCH 098/110] Implement certify-userid command --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 1 + .../cli/picocli/commands/CertifyUserIdCmd.kt | 87 +++++++++++++++++++ .../resources/msg_certify-userid.properties | 23 +++++ .../msg_certify-userid_de.properties | 20 +++++ sop-java/src/main/kotlin/sop/SOP.kt | 5 ++ .../kotlin/sop/operation/CertifyUserId.kt | 41 +++++++++ 6 files changed, 177 insertions(+) create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt create mode 100644 sop-java-picocli/src/main/resources/msg_certify-userid.properties create mode 100644 sop-java-picocli/src/main/resources/msg_certify-userid_de.properties create mode 100644 sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 4db6b30..d234e54 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -29,6 +29,7 @@ import sop.exception.SOPGPException ExtractCertCmd::class, UpdateKeyCmd::class, MergeCertsCmd::class, + CertifyUserIdCmd::class, // Messaging subcommands SignCmd::class, VerifyCmd::class, diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt new file mode 100644 index 0000000..71ef79f --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import picocli.CommandLine.Model.CommandSpec +import picocli.CommandLine.Option +import picocli.CommandLine.Parameters +import picocli.CommandLine.Spec +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.UnsupportedOption + +@Command( + name = "certify-userid", + resourceBundle = "msg_certify-userid", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class CertifyUserIdCmd : AbstractSopCmd() { + + @Spec var spec: CommandSpec? = null + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--userid"], required = true, arity = "1..*", paramLabel = "USERID") + var userIds: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + @Option(names = ["--no-require-self-sig"]) var noRequireSelfSig = false + + @Parameters(paramLabel = "KEYS", arity = "1..*") var keys: List = listOf() + + override fun run() { + val certifyUserId = + throwIfUnsupportedSubcommand(SopCLI.getSop().certifyUserId(), "certify-userid") + + if (!armor) { + certifyUserId.noArmor() + } + + if (noRequireSelfSig) { + certifyUserId.noRequireSelfSig() + } + + for (userId in userIds) { + certifyUserId.userId(userId) + } + + for (passwordFileName in withKeyPassword) { + try { + val password = stringFromInputStream(getInput(passwordFileName)) + certifyUserId.withKeyPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + for (keyInput in keys) { + try { + getInput(keyInput).use { certifyUserId.keys(it) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput) + throw BadData(errorMsg, badData) + } + } + + try { + val ready = certifyUserId.certs(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", "STDIN") + throw BadData(errorMsg, badData) + } + } +} diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid.properties b/sop-java-picocli/src/main/resources/msg_certify-userid.properties new file mode 100644 index 0000000..5eb7aa3 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_certify-userid.properties @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Certify OpenPGP Certificate User IDs +no-armor=ASCII armor the output +userid=Identities that shall be certified +with-key-password.0=Passphrase to unlock the secret key(s). +with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +no-require-self-sig=Certify the UserID regardless of whether self-certifications are present +KEYS[0..*]=Private keys + +standardInput=CERTS +standardInputDescription=Certificates that shall be certified +standardOutput=CERTS +standardOutputDescription=Certified certificates + +stacktrace=Print stacktrace +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=Parameters:%n +usage.synopsisHeading=Usage:\u0020 +usage.commandListHeading = Commands:%n +usage.optionListHeading = Options:%n +usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties new file mode 100644 index 0000000..0237fa6 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Zertifiziere OpenPGP Zertifikat Identitäten +no-armor=Schütze Ausgabe mit ASCII Armor +userid=Identität, die zertifiziert werden soll +with-key-password.0=Passwort zum Entsperren der privaten Schlüssel +with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +no-require-self-sig=Zertifiziere die Identität, unabhängig davon, ob eine Selbstzertifizierung vorhanden ist +KEYS[0..*]=Private Schlüssel + +standardInputDescription=Zertifikate, auf denen Identitäten zertifiziert werden sollen +standardOutputDescription=Zertifizierte Zertifikate + +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=Parameter:%n +usage.synopsisHeading=Aufruf:\u0020 +usage.commandListHeading=Befehle:%n +usage.optionListHeading = Optionen:%n +usage.footerHeading=Powered by Picocli%n diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index 1640d5f..5435cad 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -69,4 +69,9 @@ interface SOP : SOPV { * Merge OpenPGP certificates. */ fun mergeCerts(): MergeCerts + + /** + * Certify OpenPGP Certificate User-IDs. + */ + fun certifyUserId(): CertifyUserId } diff --git a/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt new file mode 100644 index 0000000..92fff20 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import sop.Ready +import sop.exception.SOPGPException +import sop.util.UTF8Util +import java.io.IOException +import java.io.InputStream + +interface CertifyUserId { + + @Throws(SOPGPException.UnsupportedOption::class) + fun noArmor(): CertifyUserId + + @Throws(SOPGPException.UnsupportedOption::class) + fun userId(userId: String): CertifyUserId + + @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + fun withKeyPassword(password: String): CertifyUserId = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + + @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + fun withKeyPassword(password: ByteArray): CertifyUserId + + @Throws(SOPGPException.UnsupportedOption::class) + fun noRequireSelfSig(): CertifyUserId + + @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class) + fun keys(keys: InputStream): CertifyUserId + + @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class) + fun keys(keys: ByteArray): CertifyUserId = keys(keys.inputStream()) + + @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + fun certs(certs: InputStream): Ready + + @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + fun certs(certs: ByteArray): Ready = certs(certs.inputStream()) +} \ No newline at end of file From 88d3301daad8c60ad543a7de518aa7d7b7a952e0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 16:01:58 +0200 Subject: [PATCH 099/110] Checkstyle and exception handling improvements --- .../sop/cli/picocli/commands/MergeCertsCmd.kt | 10 ++++------ .../sop/cli/picocli/commands/UpdateKeyCmd.kt | 15 +++++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt index 15b33f8..16d56e3 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt @@ -4,11 +4,11 @@ package sop.cli.picocli.commands +import java.io.IOException import picocli.CommandLine import picocli.CommandLine.Command import sop.cli.picocli.SopCLI import sop.exception.SOPGPException -import java.io.IOException @Command( name = "merge-certs", @@ -16,11 +16,9 @@ import java.io.IOException exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) class MergeCertsCmd : AbstractSopCmd() { - @CommandLine.Option(names = ["--no-armor"], negatable = true) - var armor = false + @CommandLine.Option(names = ["--no-armor"], negatable = true) var armor = false - @CommandLine.Parameters(paramLabel = "CERTS") - var updates: List = listOf() + @CommandLine.Parameters(paramLabel = "CERTS") var updates: List = listOf() override fun run() { val mergeCerts = throwIfUnsupportedSubcommand(SopCLI.getSop().mergeCerts(), "merge-certs") @@ -45,4 +43,4 @@ class MergeCertsCmd : AbstractSopCmd() { throw RuntimeException(e) } } -} \ No newline at end of file +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt index 2afa015..08f9297 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt @@ -4,11 +4,11 @@ package sop.cli.picocli.commands +import java.io.IOException import picocli.CommandLine.Command import picocli.CommandLine.Option import sop.cli.picocli.SopCLI import sop.exception.SOPGPException.* -import java.io.IOException @Command( name = "update-key", @@ -25,8 +25,7 @@ class UpdateKeyCmd : AbstractSopCmd() { @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") var withKeyPassword: List = listOf() - @Option(names = ["--merge-certs"], paramLabel = "CERTS") - var mergeCerts: List = listOf() + @Option(names = ["--merge-certs"], paramLabel = "CERTS") var mergeCerts: List = listOf() override fun run() { val updateKey = throwIfUnsupportedSubcommand(SopCLI.getSop().updateKey(), "update-key") @@ -48,7 +47,8 @@ class UpdateKeyCmd : AbstractSopCmd() { val password = stringFromInputStream(getInput(passwordFileName)) updateKey.withKeyPassword(password) } catch (unsupportedOption: UnsupportedOption) { - val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") throw UnsupportedOption(errorMsg, unsupportedOption) } catch (e: IOException) { throw RuntimeException(e) @@ -57,7 +57,7 @@ class UpdateKeyCmd : AbstractSopCmd() { for (certInput in mergeCerts) { try { - getInput(certInput).use { certIn -> updateKey.mergeCerts(certIn) } + getInput(certInput).use { updateKey.mergeCerts(it) } } catch (e: IOException) { throw RuntimeException(e) } catch (badData: BadData) { @@ -71,6 +71,9 @@ class UpdateKeyCmd : AbstractSopCmd() { ready.writeTo(System.out) } catch (e: IOException) { throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", "STDIN") + throw BadData(errorMsg, badData) } } -} \ No newline at end of file +} From 04f7a5d30dbf6680857d6f46a9d6c637c275405e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 16:56:26 +0200 Subject: [PATCH 100/110] Update msg files with input/output information --- .../src/main/resources/msg_armor.properties | 8 ++++++-- .../src/main/resources/msg_armor_de.properties | 6 ++++-- .../src/main/resources/msg_certify-userid.properties | 6 +++--- .../main/resources/msg_certify-userid_de.properties | 6 +++--- .../main/resources/msg_change-key-password.properties | 9 +++++++-- .../resources/msg_change-key-password_de.properties | 5 ++++- .../src/main/resources/msg_dearmor.properties | 9 +++++++-- .../src/main/resources/msg_dearmor_de.properties | 5 ++++- .../src/main/resources/msg_decrypt.properties | 9 +++++++-- .../src/main/resources/msg_decrypt_de.properties | 5 ++++- .../src/main/resources/msg_detached-sign.properties | 9 +++++++-- .../main/resources/msg_detached-sign_de.properties | 5 ++++- .../src/main/resources/msg_detached-verify.properties | 9 +++++++-- .../main/resources/msg_detached-verify_de.properties | 5 ++++- .../src/main/resources/msg_encrypt.properties | 9 +++++++-- .../src/main/resources/msg_encrypt_de.properties | 5 ++++- .../src/main/resources/msg_extract-cert.properties | 9 +++++++-- .../src/main/resources/msg_extract-cert_de.properties | 5 ++++- .../src/main/resources/msg_generate-key.properties | 7 +++++-- .../src/main/resources/msg_generate-key_de.properties | 4 +++- .../src/main/resources/msg_help.properties | 4 ++-- .../src/main/resources/msg_help_de.properties | 2 +- .../src/main/resources/msg_inline-detach.properties | 9 +++++++-- .../main/resources/msg_inline-detach_de.properties | 5 ++++- .../src/main/resources/msg_inline-sign.properties | 9 +++++++-- .../src/main/resources/msg_inline-sign_de.properties | 5 ++++- .../src/main/resources/msg_inline-verify.properties | 9 +++++++-- .../main/resources/msg_inline-verify_de.properties | 5 ++++- .../src/main/resources/msg_list-profiles.properties | 7 +++++-- .../main/resources/msg_list-profiles_de.properties | 4 +++- .../src/main/resources/msg_merge-certs.properties | 10 ++++++++-- .../src/main/resources/msg_merge-certs_de.properties | 10 +++++----- .../src/main/resources/msg_revoke-key.properties | 11 ++++++++--- .../src/main/resources/msg_revoke-key_de.properties | 5 ++++- .../src/main/resources/msg_sop.properties | 1 + .../src/main/resources/msg_sop_de.properties | 1 + .../src/main/resources/msg_update-key.properties | 9 +++++++-- .../src/main/resources/msg_update-key_de.properties | 5 ++++- .../src/main/resources/msg_version.properties | 6 ++++-- .../src/main/resources/msg_version_de.properties | 4 +++- 40 files changed, 190 insertions(+), 66 deletions(-) diff --git a/sop-java-picocli/src/main/resources/msg_armor.properties b/sop-java-picocli/src/main/resources/msg_armor.properties index b4dcb59..1b7c1fb 100644 --- a/sop-java-picocli/src/main/resources/msg_armor.properties +++ b/sop-java-picocli/src/main/resources/msg_armor.properties @@ -3,9 +3,13 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Add ASCII Armor to standard input +standardInput=BINARY +standardInputDescription=OpenPGP material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED) +standardOutput=ARMORED +standardOutputDescription=Same material, but with ASCII-armoring added, if not already present + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_armor_de.properties b/sop-java-picocli/src/main/resources/msg_armor_de.properties index 4c365a8..34383c8 100644 --- a/sop-java-picocli/src/main/resources/msg_armor_de.properties +++ b/sop-java-picocli/src/main/resources/msg_armor_de.properties @@ -3,9 +3,11 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Schütze Standard-Eingabe mit ASCII Armor +standardInputDescription=OpenPGP Material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED) +standardOutputDescription=Dasselbe Material, aber mit ASCII Armor kodiert, falls noch nicht geschehen + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 -usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid.properties b/sop-java-picocli/src/main/resources/msg_certify-userid.properties index 5eb7aa3..252aae4 100644 --- a/sop-java-picocli/src/main/resources/msg_certify-userid.properties +++ b/sop-java-picocli/src/main/resources/msg_certify-userid.properties @@ -16,8 +16,8 @@ standardOutputDescription=Certified certificates stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 -usage.parameterListHeading=Parameters:%n +usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = Commands:%n -usage.optionListHeading = Options:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties index 0237fa6..9f0a673 100644 --- a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties +++ b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties @@ -13,8 +13,8 @@ standardInputDescription=Zertifikate, auf denen Identit standardOutputDescription=Zertifizierte Zertifikate # Generic TODO: Remove when bumping picocli to 4.7.0 -usage.parameterListHeading=Parameter:%n +usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 -usage.commandListHeading=Befehle:%n -usage.optionListHeading = Optionen:%n +usage.commandListHeading=%nBefehle:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_change-key-password.properties b/sop-java-picocli/src/main/resources/msg_change-key-password.properties index 3de3608..79bc11b 100644 --- a/sop-java-picocli/src/main/resources/msg_change-key-password.properties +++ b/sop-java-picocli/src/main/resources/msg_change-key-password.properties @@ -12,10 +12,15 @@ old-key-password.0=Old passwords to unlock the keys with. old-key-password.1=Multiple passwords can be passed in, which are tested sequentially to unlock locked subkeys. old-key-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +standardInput=KEYS +standardInputDescription=OpenPGP keys whose passphrases shall be changed +standardOutput=KEYS +standardOutputDescription=OpenPGP keys with changed passphrases + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nDescription:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties b/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties index 014c3e7..5515c1d 100644 --- a/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties +++ b/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties @@ -12,10 +12,13 @@ old-key-password.0=Alte Passw old-key-password.1=Mehrere Passwortkandidaten können übergeben werden, welche der Reihe nach durchprobiert werden, um Unterschlüssel zu entsperren. old-key-password.2=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +standardInputDescription=OpenPGP Schlüssel deren Passwörter geändert werden sollen +standardOutputDescription=OpenPGP Schlüssel mit geänderten Passwörtern + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nBeschreibung:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_dearmor.properties b/sop-java-picocli/src/main/resources/msg_dearmor.properties index b088de1..55cbf45 100644 --- a/sop-java-picocli/src/main/resources/msg_dearmor.properties +++ b/sop-java-picocli/src/main/resources/msg_dearmor.properties @@ -3,9 +3,14 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Remove ASCII Armor from standard input +standardInput=ARMORED +standardInputDescription=Armored OpenPGP material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED) +standardOutput=BINARY +standardOutputDescription=Same material, but with ASCII-armoring removed + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_dearmor_de.properties b/sop-java-picocli/src/main/resources/msg_dearmor_de.properties index 362ccef..e01ab7a 100644 --- a/sop-java-picocli/src/main/resources/msg_dearmor_de.properties +++ b/sop-java-picocli/src/main/resources/msg_dearmor_de.properties @@ -3,9 +3,12 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Entferne ASCII Armor von Standard-Eingabe +standardInputDescription=OpenPGP Material mit ASCII Armor (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED) +standardOutputDescription=Dasselbe Material, aber mit entfernter ASCII Armor + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_decrypt.properties b/sop-java-picocli/src/main/resources/msg_decrypt.properties index 5903ded..bec315f 100644 --- a/sop-java-picocli/src/main/resources/msg_decrypt.properties +++ b/sop-java-picocli/src/main/resources/msg_decrypt.properties @@ -22,10 +22,15 @@ with-key-password.0=Passphrase to unlock the secret key(s). with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). KEY[0..*]=Secret keys to attempt decryption with +standardInput=CIPHERTEXT +standardInputDescription=Encrypted OpenPGP message +standardOutput=DATA +standardOutputDescription=Decrypted OpenPGP message + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_decrypt_de.properties b/sop-java-picocli/src/main/resources/msg_decrypt_de.properties index ba40897..395a89f 100644 --- a/sop-java-picocli/src/main/resources/msg_decrypt_de.properties +++ b/sop-java-picocli/src/main/resources/msg_decrypt_de.properties @@ -22,10 +22,13 @@ with-key-password.0=Passwort zum Entsperren der privaten Schl with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). KEY[0..*]=Private Schlüssel zum Entschlüsseln der Nachricht +standardInputDescription=Verschlüsselte OpenPGP Nachricht +standardOutputDescription=Entschlüsselte OpenPGP Nachricht + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_detached-sign.properties b/sop-java-picocli/src/main/resources/msg_detached-sign.properties index 83359a6..6ebfd0b 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-sign.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-sign.properties @@ -11,10 +11,15 @@ with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, f 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). KEYS[0..*]=Secret keys used for signing +standardInput=DATA +standardInputDescription=Data that shall be signed +standardOutput=SIGNATURES +standardOutputDescription=Detached OpenPGP signature(s) + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_detached-sign_de.properties b/sop-java-picocli/src/main/resources/msg_detached-sign_de.properties index b943da5..39b59b5 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-sign_de.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-sign_de.properties @@ -11,10 +11,13 @@ with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, 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. KEYS[0..*]=Private Signaturschlüssel +standardInputDescription=Daten die signiert werden sollen +standardOutputDescription=Abgetrennte OpenPGP Signatur(en) + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_detached-verify.properties b/sop-java-picocli/src/main/resources/msg_detached-verify.properties index ee1a468..074a318 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-verify.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-verify.properties @@ -13,11 +13,16 @@ not-after.3=Accepts special value "-" for end of time. SIGNATURE[0]=Detached signature CERT[1..*]=Public key certificates for signature verification +standardInput=DATA +standardInputDescription=Data over which the detached signatures were calculated +standardOutput=VERIFICATIONS +standardOutputDescription=Information about successfully verified signatures + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nDescription:%n usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_detached-verify_de.properties b/sop-java-picocli/src/main/resources/msg_detached-verify_de.properties index 332bff6..e21ee2a 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-verify_de.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-verify_de.properties @@ -13,11 +13,14 @@ not-after.3=Akzeptiert speziellen Wert '-' f SIGNATURE[0]=Abgetrennte Signatur CERT[1..*]=Zertifikate (öffentliche Schlüssel) zur Signaturprüfung +standardInputDescription=Daten, über die die abgetrennten Signaturen erstellt wurden +standardOutputDescription=Informationen über erfolgreich verifizierte Signaturen + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nBeschreibung:%n usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_encrypt.properties b/sop-java-picocli/src/main/resources/msg_encrypt.properties index c0f7f7d..7bbf593 100644 --- a/sop-java-picocli/src/main/resources/msg_encrypt.properties +++ b/sop-java-picocli/src/main/resources/msg_encrypt.properties @@ -12,10 +12,15 @@ with-key-password.0=Passphrase to unlock the secret key(s). with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). CERTS[0..*]=Certificates the message gets encrypted to +standardInput=DATA +standardInputDescription=Data that shall be encrypted +standardOutput=CIPHERTEXT +standardOutputDescription=Encrypted OpenPGP message + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_encrypt_de.properties b/sop-java-picocli/src/main/resources/msg_encrypt_de.properties index 6a3055c..55b0338 100644 --- a/sop-java-picocli/src/main/resources/msg_encrypt_de.properties +++ b/sop-java-picocli/src/main/resources/msg_encrypt_de.properties @@ -12,10 +12,13 @@ with-key-password.0=Passwort zum Entsperren der privaten Schl with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). CERTS[0..*]=Zertifikate für die die Nachricht verschlüsselt werden soll +standardInputDescription=Daten, die verschlüsselt werden sollen +standardOutputDescription=Verschlüsselte OpenPGP Nachricht + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_extract-cert.properties b/sop-java-picocli/src/main/resources/msg_extract-cert.properties index 82cac0f..1d1dee4 100644 --- a/sop-java-picocli/src/main/resources/msg_extract-cert.properties +++ b/sop-java-picocli/src/main/resources/msg_extract-cert.properties @@ -5,10 +5,15 @@ usage.header=Extract a public key certificate from a secret key usage.description=Read a secret key from STDIN and emit the public key certificate to STDOUT. no-armor=ASCII armor the output +standardInput=KEYS +standardInputDescription=Private key(s), from which certificate(s) shall be extracted +standardOutput=CERTS +standardOutputDescription=Extracted certificate(s) + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nDescription:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_extract-cert_de.properties b/sop-java-picocli/src/main/resources/msg_extract-cert_de.properties index 0946cfc..c92d31d 100644 --- a/sop-java-picocli/src/main/resources/msg_extract-cert_de.properties +++ b/sop-java-picocli/src/main/resources/msg_extract-cert_de.properties @@ -5,10 +5,13 @@ usage.header=Extrahiere Zertifikat ( usage.description=Lese einen Schlüssel von Standard-Eingabe und gebe das Zertifikat auf Standard-Ausgabe aus. no-armor=Schütze Ausgabe mit ASCII Armor +standardInputDescription=Private Schlüssel, deren Zertifikate extrahiert werden sollen +standardOutputDescription=Extrahierte Zertifikate + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nBeschreibung:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_generate-key.properties b/sop-java-picocli/src/main/resources/msg_generate-key.properties index 60ff4a4..c17f7f6 100644 --- a/sop-java-picocli/src/main/resources/msg_generate-key.properties +++ b/sop-java-picocli/src/main/resources/msg_generate-key.properties @@ -9,10 +9,13 @@ signing-only=Generate a key that can only be used for signing with-key-password.0=Password to protect the private key with with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +standardOutput=KEYS +standardOutputDescription=Generated OpenPGP key + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_generate-key_de.properties b/sop-java-picocli/src/main/resources/msg_generate-key_de.properties index 6a0ce13..84db04d 100644 --- a/sop-java-picocli/src/main/resources/msg_generate-key_de.properties +++ b/sop-java-picocli/src/main/resources/msg_generate-key_de.properties @@ -9,10 +9,12 @@ signing-only=Generiere einen Schl with-key-password.0=Passwort zum Schutz des privaten Schlüssels with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +standardOutputDescription=Erzeugter OpenPGP Schlüssel + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_help.properties b/sop-java-picocli/src/main/resources/msg_help.properties index 797cc79..637c1d0 100644 --- a/sop-java-picocli/src/main/resources/msg_help.properties +++ b/sop-java-picocli/src/main/resources/msg_help.properties @@ -6,6 +6,6 @@ usage.header=Display usage information for the specified subcommand stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_help_de.properties b/sop-java-picocli/src/main/resources/msg_help_de.properties index beea45c..8471188 100644 --- a/sop-java-picocli/src/main/resources/msg_help_de.properties +++ b/sop-java-picocli/src/main/resources/msg_help_de.properties @@ -7,5 +7,5 @@ stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-detach.properties b/sop-java-picocli/src/main/resources/msg_inline-detach.properties index c100c51..ca0ed6b 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-detach.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-detach.properties @@ -5,9 +5,14 @@ usage.header=Split signatures from a clearsigned message no-armor=ASCII armor the output signatures-out=Destination to which a detached signatures block will be written +standardInput=INLINESIGNED +standardInputDescription=Inline-signed OpenPGP message +standardOutput=DATA +standardOutputDescription=The message without any signatures + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-detach_de.properties b/sop-java-picocli/src/main/resources/msg_inline-detach_de.properties index e59aa34..84b8c47 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-detach_de.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-detach_de.properties @@ -5,9 +5,12 @@ usage.header=Trenne Signaturen von Klartext-signierter Nachricht no-armor=Schütze Ausgabe mit ASCII Armor signatures-out=Schreibe abgetrennte Signaturen in Ausgabe +standardInputDescription=Klartext-signierte OpenPGP Nachricht +standardOutputDescription=Nachricht ohne Signaturen + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-sign.properties b/sop-java-picocli/src/main/resources/msg_inline-sign.properties index f5143bb..936b417 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-sign.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-sign.properties @@ -13,10 +13,15 @@ with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, f 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). KEYS[0..*]=Secret keys used for signing +standardInput=DATA +standardInputDescription=Data that shall be signed +standardOutput=INLINESIGNED +standardOutputDescription=Inline-signed OpenPGP message + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-sign_de.properties b/sop-java-picocli/src/main/resources/msg_inline-sign_de.properties index b09b7e4..f8fe906 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-sign_de.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-sign_de.properties @@ -13,10 +13,13 @@ with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, 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. KEYS[0..*]=Private Signaturschlüssel +standardInputDescription=Daten, die signiert werden sollen +standardOutputDescription=Inline-signierte OpenPGP Nachricht + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-verify.properties b/sop-java-picocli/src/main/resources/msg_inline-verify.properties index dfa94d7..2e0d69f 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-verify.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-verify.properties @@ -12,10 +12,15 @@ not-after.3=Accepts special value "-" for end of time. verifications-out=File to write details over successful verifications to CERT[0..*]=Public key certificates for signature verification +standardInput=INLINESIGNED +standardInputDescription=Inline-signed OpenPGP message +standardOutput=DATA +standardOutputDescription=The message without any signatures + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-verify_de.properties b/sop-java-picocli/src/main/resources/msg_inline-verify_de.properties index a9a5722..9b70504 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-verify_de.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-verify_de.properties @@ -12,10 +12,13 @@ not-after.3=Akzeptiert speziellen Wert '-' f verifications-out=Schreibe Status der Signaturprüfung in angegebene Ausgabe CERT[0..*]=Zertifikate (öffentlich Schlüssel) zur Signaturprüfung +standardInputDescription=Inline-signierte OpenPGP Nachricht +standardOutputDescription=Nachricht ohne Signaturen + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_list-profiles.properties b/sop-java-picocli/src/main/resources/msg_list-profiles.properties index 6d5f1a8..3defe8e 100644 --- a/sop-java-picocli/src/main/resources/msg_list-profiles.properties +++ b/sop-java-picocli/src/main/resources/msg_list-profiles.properties @@ -4,10 +4,13 @@ usage.header=Emit a list of profiles supported by the identified subcommand subcommand=Subcommand for which to list profiles +standardOutput=PROFILELIST +standardOutputDescription=List of profiles supported by the identified subcommand + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_list-profiles_de.properties b/sop-java-picocli/src/main/resources/msg_list-profiles_de.properties index ac03c0d..093aeb3 100644 --- a/sop-java-picocli/src/main/resources/msg_list-profiles_de.properties +++ b/sop-java-picocli/src/main/resources/msg_list-profiles_de.properties @@ -4,10 +4,12 @@ usage.header=Gebe eine Liste von Profilen aus, welche vom angegebenen Unterbefehl unterstützt werden subcommand=Unterbefehl, für welchen Profile gelistet werden sollen +standardOutputDescription=Liste von Profilen, die der identifizierte Unterbefehl unterstützt + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_merge-certs.properties b/sop-java-picocli/src/main/resources/msg_merge-certs.properties index 866db4b..b01f577 100644 --- a/sop-java-picocli/src/main/resources/msg_merge-certs.properties +++ b/sop-java-picocli/src/main/resources/msg_merge-certs.properties @@ -6,10 +6,16 @@ usage.description=BLABLA no-armor=ASCII armor the output CERTS[0..*]=OpenPGP certificates from which updates shall be merged into the base certificates from standard input +standardInput=CERTS +standardInputDescription=Base certificates into which additional elements from the command line shall be merged +standardOutput=CERTS +standardOutputDescription=Merged certificates + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.descriptionHeading=%nNote:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties b/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties index 021c535..b1f008c 100644 --- a/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties +++ b/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties @@ -1,19 +1,19 @@ # SPDX-FileCopyrightText: 2024 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.headerHeading=OpenPGP Zertifikate zusammenführen%n%n +usage.headerHeading=OpenPGP Zertifikate zusammenführen%n usage.header=Führe OpenPGP Zertifikate aus der Standardeingabe mit ensprechenden Elementen aus CERTS zusammen und gebe das Ergebnis auf der Standardausgabe aus usage.description=Es werden nur Zertifikate auf die Standardausgabe geschrieben, welche Teil der Standardeingabe waren no-armor=Schütze Ausgabe mit ASCII Armor CERTS[0..*]=OpenPGP Zertifikate aus denen neue Elemente in die Basiszertifikate aus der Standardeingabe übernommen werden sollen -usage.parameterList.0=STANDARDIN -usage.parameterList.1=STANDARDOUT +standardInputDescription=Basis-Zertifikate, in welche zusätzliche Elemente von der Kommandozeile zusammengeführt werden sollen +standardOutputDescription=Zusammengeführte Zertifikate # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 -usage.descriptionHeading=%nHinweise:%n +usage.descriptionHeading=%nHinweis:%n usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_revoke-key.properties b/sop-java-picocli/src/main/resources/msg_revoke-key.properties index c7d72b3..f68b774 100644 --- a/sop-java-picocli/src/main/resources/msg_revoke-key.properties +++ b/sop-java-picocli/src/main/resources/msg_revoke-key.properties @@ -7,10 +7,15 @@ no-armor=ASCII armor the output with-key-password.0=Passphrase to unlock the secret key(s). with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +standardInput=KEYS +standardInputDescription=OpenPGP key that shall be revoked +standardOutput=CERTS +standardOutputDescription=Revocation certificate + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 -usage.descriptionHeading=%nDescription:%n +usage.descriptionHeading=D%nescription:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties b/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties index 95db272..fa8c5b4 100644 --- a/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties +++ b/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties @@ -7,10 +7,13 @@ no-armor=Sch with-key-password.0=Passwort zum Entsperren der privaten Schlüssel with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +standardInputDescription=OpenPGP Schlüssel, der widerrufen werden soll +standardOutputDescription=Widerrufszertifikat + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nBeschreibung:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index d5d997a..097a2e2 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -11,6 +11,7 @@ locale=Locale for description texts usage.synopsisHeading=Usage:\u0020 usage.commandListHeading=%nCommands:%n usage.optionListHeading=%nOptions:%n +usage.parameterListHeading=%nParameters:%n usage.footerHeading=Powered by picocli%n standardInputHeading=%nInput:%n diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 73efe89..99d28a7 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -11,6 +11,7 @@ locale=Gebietsschema f usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n usage.optionListHeading=%nOptionen:%n +usage.parameterListHeading=%nParameter:%n usage.footerHeading=Powered by Picocli%n standardInputHeading=%nEingabe:%n diff --git a/sop-java-picocli/src/main/resources/msg_update-key.properties b/sop-java-picocli/src/main/resources/msg_update-key.properties index dd4446d..e12fbbc 100644 --- a/sop-java-picocli/src/main/resources/msg_update-key.properties +++ b/sop-java-picocli/src/main/resources/msg_update-key.properties @@ -10,10 +10,15 @@ with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, f merge-certs.0=Merge additional elements found in the corresponding CERTS objects into the updated secret keys merge-certs.1=This can be used, for example, to absorb a third-party certification into the Transferable Secret Key +standardInput=KEYS +standardInputDescription=OpenPGP key that shall be kept up-to-date +standardOutput=KEYS +standardOutputDescription=Updated OpenPGP key + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_update-key_de.properties b/sop-java-picocli/src/main/resources/msg_update-key_de.properties index 86b999e..1b8a84d 100644 --- a/sop-java-picocli/src/main/resources/msg_update-key_de.properties +++ b/sop-java-picocli/src/main/resources/msg_update-key_de.properties @@ -10,9 +10,12 @@ with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dat merge-certs.0=Führe zusätzliche Elemente aus entsprechenden CERTS Objekten mit dem privaten Schlüssel zusammen merge-certs.1=Dies kann zum Beispiel dazu genutzt werden, Zertifizierungen dritter in den privaten Schlüssel zu übernehmen +standardInputDescription=OpenPGP Schlüssel, der auf den neusten Stand gebracht werden soll +standardOutputDescription=Erneuerter OpenPGP Schlüssel + # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_version.properties b/sop-java-picocli/src/main/resources/msg_version.properties index 9e1451b..c7d0168 100644 --- a/sop-java-picocli/src/main/resources/msg_version.properties +++ b/sop-java-picocli/src/main/resources/msg_version.properties @@ -6,9 +6,11 @@ extended=Print an extended version string backend=Print information about the cryptographic backend sop-spec=Print the latest revision of the SOP specification targeted by the implementation +standardOutput=version information + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_version_de.properties b/sop-java-picocli/src/main/resources/msg_version_de.properties index 608b0c6..c317916 100644 --- a/sop-java-picocli/src/main/resources/msg_version_de.properties +++ b/sop-java-picocli/src/main/resources/msg_version_de.properties @@ -6,9 +6,11 @@ extended=Gebe erweiterte Versionsinformationen aus backend=Gebe Informationen über das kryptografische Backend aus sop-spec=Gebe die neuste Revision der SOP Spezifikation aus, welche von dieser Implementierung umgesetzt wird +standardOutput=Versionsinformationen + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n From 6c7fcfccce6233bd8857c4ac95dd489064690103 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 16:56:25 +0200 Subject: [PATCH 101/110] Implement validate-userid command --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 1 + .../cli/picocli/commands/ValidateUserIdCmd.kt | 74 ++++++++++++++++++ .../src/main/resources/msg_sop.properties | 2 + .../resources/msg_validate-userid.properties | 18 +++++ .../msg_validate-userid_de.properties | 18 +++++ sop-java/src/main/kotlin/sop/SOP.kt | 15 ++-- .../kotlin/sop/exception/SOPGPException.kt | 27 +++++-- .../kotlin/sop/operation/ValidateUserId.kt | 78 +++++++++++++++++++ 8 files changed, 216 insertions(+), 17 deletions(-) create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt create mode 100644 sop-java-picocli/src/main/resources/msg_validate-userid.properties create mode 100644 sop-java-picocli/src/main/resources/msg_validate-userid_de.properties create mode 100644 sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index d234e54..dc14907 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -30,6 +30,7 @@ import sop.exception.SOPGPException UpdateKeyCmd::class, MergeCertsCmd::class, CertifyUserIdCmd::class, + ValidateUserIdCmd::class, // Messaging subcommands SignCmd::class, VerifyCmd::class, diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt new file mode 100644 index 0000000..c2de148 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import picocli.CommandLine.Parameters +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.util.HexUtil.Companion.bytesToHex + +@Command( + name = "validate-userid", + resourceBundle = "msg_validate-userid", + exitCodeOnInvalidInput = SOPGPException.MissingArg.EXIT_CODE) +class ValidateUserIdCmd : AbstractSopCmd() { + + @Option(names = ["--addr-spec-only"]) var addrSpecOnly: Boolean = false + + @Parameters(index = "0", arity = "1", paramLabel = "USERID") lateinit var userId: String + + @Parameters(index = "1..*", arity = "1..*", paramLabel = "CERTS") + var authorities: List = listOf() + + override fun run() { + val validateUserId = + throwIfUnsupportedSubcommand(SopCLI.getSop().validateUserId(), "validate-userid") + + if (addrSpecOnly) { + validateUserId.addrSpecOnly() + } + + validateUserId.userId(userId) + + for (authority in authorities) { + try { + getInput(authority).use { validateUserId.authorities(it) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (b: SOPGPException.BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", authority) + throw SOPGPException.BadData(errorMsg, b) + } + } + + try { + val valid = validateUserId.subjects(System.`in`) + + if (!valid) { + val errorMsg = getMsg("sop.error.runtime.any_cert_user_id_no_match", userId) + throw SOPGPException.CertUserIdNoMatch(errorMsg) + } + } catch (e: SOPGPException.CertUserIdNoMatch) { + val errorMsg = + if (e.fingerprint != null) { + getMsg( + "sop.error.runtime.cert_user_id_no_match", + bytesToHex(e.fingerprint!!), + userId) + } else { + getMsg("sop.error.runtime.any_cert_user_id_no_match", userId) + } + throw SOPGPException.CertUserIdNoMatch(errorMsg, e) + } catch (e: SOPGPException.BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", "STDIN") + throw SOPGPException.BadData(errorMsg, e) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 097a2e2..520533a 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -80,6 +80,8 @@ sop.error.runtime.cert_cannot_encrypt=Certificate from input '%s' 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. sop.error.runtime.cannot_decrypt_message=Message could not be decrypted. +sop.error.runtime.cert_user_id_no_match=Certificate '%s' does not contain a valid binding for user id '%s'. +sop.error.runtime.any_cert_user_id_no_match=Any certificate does not contain a valid binding for user id '%s'. ## 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 '%s' is required. diff --git a/sop-java-picocli/src/main/resources/msg_validate-userid.properties b/sop-java-picocli/src/main/resources/msg_validate-userid.properties new file mode 100644 index 0000000..5cfed2d --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_validate-userid.properties @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Validate a UserID in an OpenPGP certificate +addr-spec-only=Treat the USERID as an email address, match only against the email address part of each correctly bound UserID +USERID[0]=UserID +CERTS[1..*]=Authority OpenPGP certificates + +standardInput=CERTS +standardInputDescription=OpenPGP certificates in which UserID bindings shall be validated + +stacktrace=Print stacktrace +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=%nParameters:%n +usage.synopsisHeading=Usage:\u0020 +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n +usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties b/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties new file mode 100644 index 0000000..8231c6a --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Validiere eine UserID auf OpenPGP Zertifikaten +addr-spec-only=Behandle die USERID als E-Mail-Adresse, vergleiche sie nur mit dem E-Mail-Adressen-Teil jeder korrekten UserID +USERID[0]=UserID +CERTS[1..*]=Autoritäre OpenPGP Zertifikate + +standardInput=CERTS +standardInputDescription=OpenPGP Zertifikate auf denen UserIDs validiert werden sollen + +stacktrace=Print stacktrace +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=%nParameter:%n +usage.synopsisHeading=Aufruf:\u0020 +usage.commandListHeading=%nBefehle:%n +usage.optionListHeading=%nOptionen:%n +usage.footerHeading=Powered by Picocli%n diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index 5435cad..c5f05e2 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -60,18 +60,15 @@ interface SOP : SOPV { /** Update a key's password. */ fun changeKeyPassword(): ChangeKeyPassword - /** - * Keep a secret key up-to-date. - */ + /** Keep a secret key up-to-date. */ fun updateKey(): UpdateKey - /** - * Merge OpenPGP certificates. - */ + /** Merge OpenPGP certificates. */ fun mergeCerts(): MergeCerts - /** - * Certify OpenPGP Certificate User-IDs. - */ + /** Certify OpenPGP Certificate User-IDs. */ fun certifyUserId(): CertifyUserId + + /** Validate a UserID in an OpenPGP certificate. */ + fun validateUserId(): ValidateUserId } diff --git a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt index 1f9ce6b..862e1bd 100644 --- a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt +++ b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt @@ -338,9 +338,7 @@ abstract class SOPGPException : RuntimeException { } } - /** - * The primary key of a KEYS object is too weak or revoked. - */ + /** The primary key of a KEYS object is too weak or revoked. */ class PrimaryKeyBad : SOPGPException { constructor() : super() @@ -353,13 +351,26 @@ abstract class SOPGPException : RuntimeException { } } - /** - * The CERTS object has no matching User ID. - */ + /** The CERTS object has no matching User ID. */ class CertUserIdNoMatch : SOPGPException { - constructor() : super() - constructor(errorMsg: String) : super(errorMsg) + val fingerprint: ByteArray? + + constructor() : super() { + fingerprint = null + } + + constructor(fingerprint: ByteArray) : super() { + this.fingerprint = fingerprint + } + + constructor(errorMsg: String) : super(errorMsg) { + fingerprint = null + } + + constructor(errorMsg: String, cause: Throwable) : super(errorMsg, cause) { + fingerprint = null + } override fun getExitCode(): Int = EXIT_CODE diff --git a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt new file mode 100644 index 0000000..4f4c51a --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.exception.SOPGPException + +/** Subcommand to validate UserIDs on certificates. */ +interface ValidateUserId { + + /** + * If this is set, then the USERID is treated as an e-mail address, and matched only against the + * e-mail address part of each correctly bound User ID. The rest of each correctly bound User ID + * is ignored. + * + * @return this + */ + @Throws(SOPGPException.UnsupportedOption::class) fun addrSpecOnly(): ValidateUserId + + /** + * Set the UserID to validate. To match only the email address, call [addrSpecOnly]. + * + * @param userId UserID or email address + * @return this + */ + fun userId(userId: String): ValidateUserId + + /** + * Add certificates, which act as authorities. The [userId] is only considered correctly bound, + * if it was bound by an authoritative certificate. + * + * @param certs authoritative certificates + * @return this + */ + @Throws(SOPGPException.BadData::class, IOException::class) + fun authorities(certs: InputStream): ValidateUserId + + /** + * Add certificates, which act as authorities. The [userId] is only considered correctly bound, + * if it was bound by an authoritative certificate. + * + * @param certs authoritative certificates + * @return this + */ + @Throws(SOPGPException.BadData::class, IOException::class) + fun authorities(certs: ByteArray): ValidateUserId = authorities(certs.inputStream()) + + /** + * Add subject certificates, on which UserID bindings are validated. + * + * @param certs subject certificates + * @return true if all subject certificates have a correct binding to the UserID. + * @throws SOPGPException.BadData if the subject certificates are malformed + * @throws IOException if a parser exception happens + * @throws SOPGPException.CertUserIdNoMatch if any subject certificate does not have a correctly + * bound UserID that matches [userId]. + */ + @Throws( + SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + fun subjects(certs: InputStream): Boolean + + /** + * Add subject certificates, on which UserID bindings are validated. + * + * @param certs subject certificates + * @return true if all subject certificates have a correct binding to the UserID. + * @throws SOPGPException.BadData if the subject certificates are malformed + * @throws IOException if a parser exception happens + * @throws SOPGPException.CertUserIdNoMatch if any subject certificate does not have a correctly + * bound UserID that matches [userId]. + */ + @Throws( + SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + fun subjects(certs: ByteArray): Boolean = subjects(certs.inputStream()) +} From 83eafd881782535ff61f03fe9a2cb301cd2a686a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 16:56:43 +0200 Subject: [PATCH 102/110] Checkstyle --- .../kotlin/sop/operation/CertifyUserId.kt | 24 ++++++++--------- .../main/kotlin/sop/operation/MergeCerts.kt | 9 +++---- .../main/kotlin/sop/operation/UpdateKey.kt | 27 +++++++++++++------ 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt index 92fff20..642966b 100644 --- a/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt +++ b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt @@ -4,28 +4,26 @@ package sop.operation +import java.io.IOException +import java.io.InputStream import sop.Ready import sop.exception.SOPGPException import sop.util.UTF8Util -import java.io.IOException -import java.io.InputStream interface CertifyUserId { - @Throws(SOPGPException.UnsupportedOption::class) - fun noArmor(): CertifyUserId + @Throws(SOPGPException.UnsupportedOption::class) fun noArmor(): CertifyUserId - @Throws(SOPGPException.UnsupportedOption::class) - fun userId(userId: String): CertifyUserId + @Throws(SOPGPException.UnsupportedOption::class) fun userId(userId: String): CertifyUserId @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) - fun withKeyPassword(password: String): CertifyUserId = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + fun withKeyPassword(password: String): CertifyUserId = + withKeyPassword(password.toByteArray(UTF8Util.UTF8)) @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) fun withKeyPassword(password: ByteArray): CertifyUserId - @Throws(SOPGPException.UnsupportedOption::class) - fun noRequireSelfSig(): CertifyUserId + @Throws(SOPGPException.UnsupportedOption::class) fun noRequireSelfSig(): CertifyUserId @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class) fun keys(keys: InputStream): CertifyUserId @@ -33,9 +31,11 @@ interface CertifyUserId { @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class) fun keys(keys: ByteArray): CertifyUserId = keys(keys.inputStream()) - @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + @Throws( + SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) fun certs(certs: InputStream): Ready - @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + @Throws( + SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) fun certs(certs: ByteArray): Ready = certs(certs.inputStream()) -} \ No newline at end of file +} diff --git a/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt index f60d291..f922490 100644 --- a/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt +++ b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt @@ -4,15 +4,14 @@ package sop.operation -import sop.Ready -import sop.exception.SOPGPException import java.io.IOException import java.io.InputStream +import sop.Ready +import sop.exception.SOPGPException interface MergeCerts { - @Throws(SOPGPException.UnsupportedOption::class) - fun noArmor(): MergeCerts + @Throws(SOPGPException.UnsupportedOption::class) fun noArmor(): MergeCerts @Throws(SOPGPException.BadData::class, IOException::class) fun updates(updateCerts: InputStream): MergeCerts @@ -25,4 +24,4 @@ interface MergeCerts { @Throws(SOPGPException.BadData::class, IOException::class) fun baseCertificates(certs: ByteArray): Ready = baseCertificates(certs.inputStream()) -} \ No newline at end of file +} diff --git a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt index 1b12f6f..6c32b22 100644 --- a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt @@ -4,11 +4,11 @@ package sop.operation +import java.io.IOException +import java.io.InputStream import sop.Ready import sop.exception.SOPGPException import sop.util.UTF8Util -import java.io.IOException -import java.io.InputStream interface UpdateKey { @@ -24,20 +24,31 @@ interface UpdateKey { @Throws(SOPGPException.UnsupportedOption::class) fun noNewMechanisms(): UpdateKey @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) - fun withKeyPassword(password: String): UpdateKey = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + fun withKeyPassword(password: String): UpdateKey = + withKeyPassword(password.toByteArray(UTF8Util.UTF8)) @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) fun withKeyPassword(password: ByteArray): UpdateKey - @Throws(SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) + @Throws( + SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) fun mergeCerts(certs: InputStream): UpdateKey - @Throws(SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) + @Throws( + SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) fun mergeCerts(certs: ByteArray): UpdateKey = mergeCerts(certs.inputStream()) - @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class, SOPGPException.PrimaryKeyBad::class) + @Throws( + SOPGPException.BadData::class, + IOException::class, + SOPGPException.KeyIsProtected::class, + SOPGPException.PrimaryKeyBad::class) fun key(key: InputStream): Ready - @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class, SOPGPException.PrimaryKeyBad::class) + @Throws( + SOPGPException.BadData::class, + IOException::class, + SOPGPException.KeyIsProtected::class, + SOPGPException.PrimaryKeyBad::class) fun key(key: ByteArray): Ready = key(key.inputStream()) -} \ No newline at end of file +} From 54782d2a22af7e24935ecf037b98ac0d8f0a26c1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 17:51:04 +0200 Subject: [PATCH 103/110] Implement external variants of new subcommands --- .../main/kotlin/sop/external/ExternalSOP.kt | 20 ++++++++ .../operation/CertifyUserIdExternal.kt | 48 +++++++++++++++++++ .../external/operation/MergeCertsExternal.kt | 30 ++++++++++++ .../external/operation/UpdateKeyExternal.kt | 41 ++++++++++++++++ .../operation/ValidateUserIdExternal.kt | 38 +++++++++++++++ 5 files changed, 177 insertions(+) create mode 100644 external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt create mode 100644 external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt create mode 100644 external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt create mode 100644 external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt index 27c93ae..8ab7737 100644 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -69,6 +69,14 @@ class ExternalSOP( override fun changeKeyPassword(): ChangeKeyPassword = ChangeKeyPasswordExternal(binaryName, properties) + override fun updateKey(): UpdateKey = UpdateKeyExternal(binaryName, properties) + + override fun mergeCerts(): MergeCerts = MergeCertsExternal(binaryName, properties) + + override fun certifyUserId(): CertifyUserId = CertifyUserIdExternal(binaryName, properties) + + override fun validateUserId(): ValidateUserId = ValidateUserIdExternal(binaryName, properties) + /** * This interface can be used to provide a directory in which external SOP binaries can * temporarily store additional results of OpenPGP operations such that the binding classes can @@ -169,6 +177,18 @@ class ExternalSOP( UnsupportedProfile.EXIT_CODE -> throw UnsupportedProfile( "External SOP backend reported error UnsupportedProfile ($exitCode):\n$errorMessage") + NoHardwareKeyFound.EXIT_CODE -> + throw NoHardwareKeyFound( + "External SOP backend reported error NoHardwareKeyFound ($exitCode):\n$errorMessage") + HardwareKeyFailure.EXIT_CODE -> + throw HardwareKeyFailure( + "External SOP backend reported error HardwareKeyFalure ($exitCode):\n$errorMessage") + PrimaryKeyBad.EXIT_CODE -> + throw PrimaryKeyBad( + "External SOP backend reported error PrimaryKeyBad ($exitCode):\n$errorMessage") + CertUserIdNoMatch.EXIT_CODE -> + throw CertUserIdNoMatch( + "External SOP backend reported error CertUserIdNoMatch ($exitCode):\n$errorMessage") // Did you forget to add a case for a new exception type? else -> diff --git a/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt new file mode 100644 index 0000000..abf4d50 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.* +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.CertifyUserId + +class CertifyUserIdExternal(binary: String, environment: Properties) : CertifyUserId { + + private val commandList = mutableListOf(binary, "version") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCount = 0 + + private val keys: MutableList = mutableListOf() + + override fun noArmor(): CertifyUserId = apply { commandList.add("--no-armor") } + + override fun userId(userId: String): CertifyUserId = apply { + commandList.add("--userid") + commandList.add(userId) + } + + override fun withKeyPassword(password: ByteArray): CertifyUserId = apply { + commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount") + envList.add("KEY_PASSWORD_$argCount=${String(password)}") + argCount += 1 + } + + override fun noRequireSelfSig(): CertifyUserId = apply { + commandList.add("--no-require-self-sig") + } + + override fun keys(keys: InputStream): CertifyUserId = apply { + this.keys.add("@ENV:KEY_$argCount") + envList.add("KEY_$argCount=${ExternalSOP.readString(keys)}") + argCount += 1 + } + + override fun certs(certs: InputStream): Ready = + ExternalSOP.executeTransformingOperation( + Runtime.getRuntime(), commandList.plus(keys), envList, certs) +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt new file mode 100644 index 0000000..0869fab --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.* +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.MergeCerts + +class MergeCertsExternal(binary: String, environment: Properties) : MergeCerts { + + private val commandList = mutableListOf(binary, "version") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCount = 0 + + override fun noArmor(): MergeCerts = apply { commandList.add("--no-armor") } + + override fun updates(updateCerts: InputStream): MergeCerts = apply { + commandList.add("@ENV:CERT_$argCount") + envList.add("CERT_$argCount=${ExternalSOP.readString(updateCerts)}") + argCount += 1 + } + + override fun baseCertificates(certs: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, certs) +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt new file mode 100644 index 0000000..9aa1d29 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.* +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.UpdateKey + +class UpdateKeyExternal(binary: String, environment: Properties) : UpdateKey { + + private val commandList = mutableListOf(binary, "update-key") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCount = 0 + + override fun noArmor(): UpdateKey = apply { commandList.add("--no-armor") } + + override fun signingOnly(): UpdateKey = apply { commandList.add("--signing-only") } + + override fun noNewMechanisms(): UpdateKey = apply { commandList.add("--no-new-mechanisms") } + + override fun withKeyPassword(password: ByteArray): UpdateKey = apply { + commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount") + envList.add("KEY_PASSWORD_$argCount=${String(password)}") + argCount += 1 + } + + override fun mergeCerts(certs: InputStream): UpdateKey = apply { + commandList.add("--merge-certs") + commandList.add("@ENV:CERT_$argCount") + envList.add("CERT_$argCount=${ExternalSOP.readString(certs)}") + argCount += 1 + } + + override fun key(key: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, key) +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt new file mode 100644 index 0000000..867a755 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.* +import sop.external.ExternalSOP +import sop.operation.ValidateUserId + +class ValidateUserIdExternal(binary: String, environment: Properties) : ValidateUserId { + + private val commandList = mutableListOf(binary, "version") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCount = 0 + + private var userId: String? = null + private val authorities: MutableList = mutableListOf() + + override fun addrSpecOnly(): ValidateUserId = apply { commandList.add("--addr-spec-only") } + + override fun userId(userId: String): ValidateUserId = apply { this.userId = userId } + + override fun authorities(certs: InputStream): ValidateUserId = apply { + this.authorities.add("@ENV:CERT_$argCount") + envList.add("CERT_$argCount=${ExternalSOP.readString(certs)}") + argCount += 1 + } + + override fun subjects(certs: InputStream): Boolean { + ExternalSOP.executeTransformingOperation( + Runtime.getRuntime(), commandList.plus(userId!!).plus(authorities), envList, certs) + .bytes + return true + } +} From 3367450f944ec7c847f0279dd6dd0f0fd80fd37c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 18:25:03 +0200 Subject: [PATCH 104/110] Show endOfOptions delimiter in help --- .../kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt | 7 ++----- .../kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt | 3 ++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt index 71ef79f..228809b 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt @@ -6,10 +6,8 @@ package sop.cli.picocli.commands import java.io.IOException import picocli.CommandLine.Command -import picocli.CommandLine.Model.CommandSpec import picocli.CommandLine.Option import picocli.CommandLine.Parameters -import picocli.CommandLine.Spec import sop.cli.picocli.SopCLI import sop.exception.SOPGPException.BadData import sop.exception.SOPGPException.UnsupportedOption @@ -17,11 +15,10 @@ import sop.exception.SOPGPException.UnsupportedOption @Command( name = "certify-userid", resourceBundle = "msg_certify-userid", - exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE, + showEndOfOptionsDelimiterInUsageHelp = true) class CertifyUserIdCmd : AbstractSopCmd() { - @Spec var spec: CommandSpec? = null - @Option(names = ["--no-armor"], negatable = true) var armor = true @Option(names = ["--userid"], required = true, arity = "1..*", paramLabel = "USERID") diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt index c2de148..da81a27 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt @@ -15,7 +15,8 @@ import sop.util.HexUtil.Companion.bytesToHex @Command( name = "validate-userid", resourceBundle = "msg_validate-userid", - exitCodeOnInvalidInput = SOPGPException.MissingArg.EXIT_CODE) + exitCodeOnInvalidInput = SOPGPException.MissingArg.EXIT_CODE, + showEndOfOptionsDelimiterInUsageHelp = true) class ValidateUserIdCmd : AbstractSopCmd() { @Option(names = ["--addr-spec-only"]) var addrSpecOnly: Boolean = false From f6d7af2dabe81d800964ebc8fd0f62a82ba5bb8d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 18:25:17 +0200 Subject: [PATCH 105/110] Add first test for new commands --- .../test/java/sop/cli/picocli/SOPTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) 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 fe49472..4d36322 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 @@ -13,10 +13,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import sop.SOP; import sop.exception.SOPGPException; import sop.operation.Armor; +import sop.operation.CertifyUserId; import sop.operation.ChangeKeyPassword; import sop.operation.Dearmor; import sop.operation.Decrypt; @@ -29,7 +31,10 @@ import sop.operation.InlineVerify; import sop.operation.DetachedSign; import sop.operation.DetachedVerify; import sop.operation.ListProfiles; +import sop.operation.MergeCerts; import sop.operation.RevokeKey; +import sop.operation.UpdateKey; +import sop.operation.ValidateUserId; import sop.operation.Version; public class SOPTest { @@ -52,6 +57,30 @@ public class SOPTest { @Test public void UnsupportedSubcommandsTest() { SOP nullCommandSOP = new SOP() { + @NotNull + @Override + public ValidateUserId validateUserId() { + return null; + } + + @NotNull + @Override + public CertifyUserId certifyUserId() { + return null; + } + + @NotNull + @Override + public MergeCerts mergeCerts() { + return null; + } + + @NotNull + @Override + public UpdateKey updateKey() { + return null; + } + @Override public Version version() { return null; @@ -140,6 +169,11 @@ public class SOPTest { commands.add(new String[] {"sign"}); commands.add(new String[] {"verify", "signature.asc", "cert.asc"}); commands.add(new String[] {"version"}); + commands.add(new String[] {"list-profiles", "generate-key"}); + commands.add(new String[] {"certify-userid", "--userid", "Alice ", "--", "alice.pgp"}); + commands.add(new String[] {"validate-userid", "Alice ", "bob.pgp", "--", "alice.pgp"}); + commands.add(new String[] {"update-key"}); + commands.add(new String[] {"merge-certs"}); for (String[] command : commands) { int exit = SopCLI.execute(command); From 966ea3c3a66209e1132fe84e7b93dfb9182d6c7f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 18:40:55 +0200 Subject: [PATCH 106/110] Fix nullability of sop commands --- .../test/java/sop/cli/picocli/SOPTest.java | 5 --- sop-java/src/main/kotlin/sop/SOP.kt | 34 +++++++++---------- sop-java/src/main/kotlin/sop/SOPV.kt | 8 ++--- 3 files changed, 21 insertions(+), 26 deletions(-) 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 4d36322..62c7581 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 @@ -13,7 +13,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import sop.SOP; import sop.exception.SOPGPException; @@ -57,25 +56,21 @@ public class SOPTest { @Test public void UnsupportedSubcommandsTest() { SOP nullCommandSOP = new SOP() { - @NotNull @Override public ValidateUserId validateUserId() { return null; } - @NotNull @Override public CertifyUserId certifyUserId() { return null; } - @NotNull @Override public MergeCerts mergeCerts() { return null; } - @NotNull @Override public UpdateKey updateKey() { return null; diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index c5f05e2..fbd0428 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -15,60 +15,60 @@ import sop.operation.* interface SOP : SOPV { /** Generate a secret key. */ - fun generateKey(): GenerateKey + fun generateKey(): GenerateKey? /** Extract a certificate (public key) from a secret key. */ - fun extractCert(): ExtractCert + fun extractCert(): ExtractCert? /** * Create detached signatures. If you want to sign a message inline, use [inlineSign] instead. */ - fun sign(): DetachedSign = detachedSign() + fun sign(): DetachedSign? = detachedSign() /** * Create detached signatures. If you want to sign a message inline, use [inlineSign] instead. */ - fun detachedSign(): DetachedSign + fun detachedSign(): DetachedSign? /** * Sign a message using inline signatures. If you need to create detached signatures, use * [detachedSign] instead. */ - fun inlineSign(): InlineSign + fun inlineSign(): InlineSign? /** Detach signatures from an inline signed message. */ - fun inlineDetach(): InlineDetach + fun inlineDetach(): InlineDetach? /** Encrypt a message. */ - fun encrypt(): Encrypt + fun encrypt(): Encrypt? /** Decrypt a message. */ - fun decrypt(): Decrypt + fun decrypt(): Decrypt? /** Convert binary OpenPGP data to ASCII. */ - fun armor(): Armor + fun armor(): Armor? /** Converts ASCII armored OpenPGP data to binary. */ - fun dearmor(): Dearmor + fun dearmor(): Dearmor? /** List supported [Profiles][Profile] of a subcommand. */ - fun listProfiles(): ListProfiles + fun listProfiles(): ListProfiles? /** Revoke one or more secret keys. */ - fun revokeKey(): RevokeKey + fun revokeKey(): RevokeKey? /** Update a key's password. */ - fun changeKeyPassword(): ChangeKeyPassword + fun changeKeyPassword(): ChangeKeyPassword? /** Keep a secret key up-to-date. */ - fun updateKey(): UpdateKey + fun updateKey(): UpdateKey? /** Merge OpenPGP certificates. */ - fun mergeCerts(): MergeCerts + fun mergeCerts(): MergeCerts? /** Certify OpenPGP Certificate User-IDs. */ - fun certifyUserId(): CertifyUserId + fun certifyUserId(): CertifyUserId? /** Validate a UserID in an OpenPGP certificate. */ - fun validateUserId(): ValidateUserId + fun validateUserId(): ValidateUserId? } diff --git a/sop-java/src/main/kotlin/sop/SOPV.kt b/sop-java/src/main/kotlin/sop/SOPV.kt index d331559..58a7f13 100644 --- a/sop-java/src/main/kotlin/sop/SOPV.kt +++ b/sop-java/src/main/kotlin/sop/SOPV.kt @@ -12,23 +12,23 @@ import sop.operation.Version interface SOPV { /** Get information about the implementations name and version. */ - fun version(): Version + fun version(): Version? /** * Verify detached signatures. If you need to verify an inline-signed message, use * [inlineVerify] instead. */ - fun verify(): DetachedVerify = detachedVerify() + fun verify(): DetachedVerify? = detachedVerify() /** * Verify detached signatures. If you need to verify an inline-signed message, use * [inlineVerify] instead. */ - fun detachedVerify(): DetachedVerify + fun detachedVerify(): DetachedVerify? /** * Verify signatures of an inline-signed message. If you need to verify detached signatures over * a message, use [detachedVerify] instead. */ - fun inlineVerify(): InlineVerify + fun inlineVerify(): InlineVerify? } From db315e9ed2079c86cbfefdf097286e84b99aef6a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 19:19:56 +0200 Subject: [PATCH 107/110] Document endOfOptionsDelimiter --- .../src/main/resources/msg_certify-userid.properties | 2 ++ .../src/main/resources/msg_certify-userid_de.properties | 2 ++ .../src/main/resources/msg_validate-userid.properties | 2 ++ .../src/main/resources/msg_validate-userid_de.properties | 2 ++ 4 files changed, 8 insertions(+) diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid.properties b/sop-java-picocli/src/main/resources/msg_certify-userid.properties index 252aae4..36dc6f4 100644 --- a/sop-java-picocli/src/main/resources/msg_certify-userid.properties +++ b/sop-java-picocli/src/main/resources/msg_certify-userid.properties @@ -14,6 +14,8 @@ standardInputDescription=Certificates that shall be certified standardOutput=CERTS standardOutputDescription=Certified certificates +picocli.endofoptions.description=End of options. Remainder are positional parameters. Fixes 'Missing required parameter' error + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties index 9f0a673..d634c59 100644 --- a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties +++ b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties @@ -12,6 +12,8 @@ KEYS[0..*]=Private Schl standardInputDescription=Zertifikate, auf denen Identitäten zertifiziert werden sollen standardOutputDescription=Zertifizierte Zertifikate +picocli.endofoptions.description=Ende der Optionen. Der Rest sind Positionsparameter. Behebt 'Missing required parameter' Fehler + # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 diff --git a/sop-java-picocli/src/main/resources/msg_validate-userid.properties b/sop-java-picocli/src/main/resources/msg_validate-userid.properties index 5cfed2d..d25fa3a 100644 --- a/sop-java-picocli/src/main/resources/msg_validate-userid.properties +++ b/sop-java-picocli/src/main/resources/msg_validate-userid.properties @@ -9,6 +9,8 @@ CERTS[1..*]=Authority OpenPGP certificates standardInput=CERTS standardInputDescription=OpenPGP certificates in which UserID bindings shall be validated +picocli.endofoptions.description=End of options. Remainder are positional parameters. Fixes 'Missing required parameter' error + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n diff --git a/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties b/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties index 8231c6a..f919465 100644 --- a/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties +++ b/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties @@ -9,6 +9,8 @@ CERTS[1..*]=Autorit standardInput=CERTS standardInputDescription=OpenPGP Zertifikate auf denen UserIDs validiert werden sollen +picocli.endofoptions.description=Ende der Optionen. Der Rest sind Positionsparameter. Behebt 'Missing required parameter' Fehler + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n From 0b68d9f04e98d32715c61fe6b871ba135c293624 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:26:36 +0200 Subject: [PATCH 108/110] Bump version --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 33a2251..f98a318 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '10.1.1' + shortVersion = '11.0.0' isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 From b42b9668669fa4531090184c64eecb1a0af0d903 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:49:00 +0200 Subject: [PATCH 109/110] Fix documentation of merge-certs command --- sop-java-picocli/src/main/resources/msg_merge-certs.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sop-java-picocli/src/main/resources/msg_merge-certs.properties b/sop-java-picocli/src/main/resources/msg_merge-certs.properties index b01f577..8c0bfa3 100644 --- a/sop-java-picocli/src/main/resources/msg_merge-certs.properties +++ b/sop-java-picocli/src/main/resources/msg_merge-certs.properties @@ -2,7 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 usage.headerHeading=Merge OpenPGP certificates%n -usage.description=BLABLA +usage.header=Merge OpenPGP certificates from standard input with related elements from CERTS and emit the result to standard output +usage.description=Only certificates that were part of standard input will be emitted to standard output no-armor=ASCII armor the output CERTS[0..*]=OpenPGP certificates from which updates shall be merged into the base certificates from standard input From 7ee856f9b9db1da18543590ca3ebdea23608e182 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 8 Apr 2025 13:13:15 +0200 Subject: [PATCH 110/110] Remove call to explicitly set bundle to fix native image --- sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index dc14907..943f0f3 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -85,8 +85,6 @@ class SopCLI { return CommandLine(SopCLI::class.java) .apply { - // explicitly set help command resource bundle - subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help")) // Hide generate-completion command subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true) // render Input/Output sections in help command