From 42bd8f06a410358c79ed24a1cf33cbfc73fb98a9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 14:41:39 +0200 Subject: [PATCH 001/298] SOP-Java 5.0.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index bf632fc..3088539 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '5.0.0' - isSnapshot = false + shortVersion = '5.0.1' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 925505989e10cdf8aa3defe74d2adb269efd8424 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 14:22:26 +0200 Subject: [PATCH 002/298] Bump version to 6.0.0 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 3088539..7202f0a 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '5.0.1' + shortVersion = '6.0.0' isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 From dfce1ad6bbaa5d11dc8dd9793e527579e8ba2535 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 14:23:04 +0200 Subject: [PATCH 003/298] Bump SOP spec to 06 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a4ad20..d130a48 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: 5](https://img.shields.io/badge/Spec%20Revision-5-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/05/) +[![Spec Revision: 6](https://img.shields.io/badge/Spec%20Revision-6-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/06/) [![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 f49c16e4c5a28564312a5c912ec26e3009a5bae9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 14:41:37 +0200 Subject: [PATCH 004/298] Implement sop version --sop-spec --- .../external/operation/VersionExternal.java | 19 +++++++++++++++++++ .../sop/cli/picocli/commands/VersionCmd.java | 8 ++++++++ .../src/main/java/sop/operation/Version.java | 10 ++++++++++ .../sop/testsuite/operation/VersionTest.java | 8 ++++++++ 4 files changed, 45 insertions(+) diff --git a/external-sop/src/main/java/sop/external/operation/VersionExternal.java b/external-sop/src/main/java/sop/external/operation/VersionExternal.java index 6848cab..95dc8f5 100644 --- a/external-sop/src/main/java/sop/external/operation/VersionExternal.java +++ b/external-sop/src/main/java/sop/external/operation/VersionExternal.java @@ -99,4 +99,23 @@ public class VersionExternal implements Version { throw new RuntimeException(e); } } + + @Override + public String getSopSpecVersion() { + String[] command = new String[] {binary, "version", "--sop-spec"}; + String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); + try { + Process process = runtime.exec(command, env); + BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = stdInput.readLine()) != null) { + sb.append(line).append('\n'); + } + ExternalSOP.finish(process); + return sb.toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java index 3610ff4..c11d050 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java @@ -22,6 +22,9 @@ public class VersionCmd extends AbstractSopCmd { @CommandLine.Option(names = "--backend") boolean backend; + + @CommandLine.Option(names = "--sop-spec") + boolean sopSpec; } @@ -45,5 +48,10 @@ public class VersionCmd extends AbstractSopCmd { Print.outln(version.getBackendVersion()); return; } + + if (exclusive.sopSpec) { + Print.outln(version.getSopSpecVersion()); + return; + } } } diff --git a/sop-java/src/main/java/sop/operation/Version.java b/sop-java/src/main/java/sop/operation/Version.java index 0b50993..1cd2698 100644 --- a/sop-java/src/main/java/sop/operation/Version.java +++ b/sop-java/src/main/java/sop/operation/Version.java @@ -46,4 +46,14 @@ public interface Version { * @return extended version string */ String getExtendedVersion(); + + /** + * Return the revision of the SOP specification that this implementation is implementing, for example, + *
draft-dkg-openpgp-stateless-cli-06
. + * If the implementation targets a specific draft but the implementer knows the implementation is incomplete, + * it should prefix the draft title with a "~" (TILDE, U+007E), for example:
~draft-dkg-openpgp-stateless-cli-06
. + * + * @return implemented SOP spec version + */ + String getSopSpecVersion(); } 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 16aff4d..5ad1a52 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java @@ -14,6 +14,7 @@ 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; @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") public class VersionTest extends AbstractSOPTest { @@ -51,4 +52,11 @@ public class VersionTest extends AbstractSOPTest { assertFalse(extended.isEmpty()); } + @ParameterizedTest + @MethodSource("provideInstances") + public void sopSpecVersionTest(SOP sop) { + String sopSpec = sop.version().getSopSpecVersion(); + assertTrue(sopSpec.startsWith("draft-dkg-openpgp-stateless-cli-") || + sopSpec.startsWith("~draft-dkg-openpgp-stateless-cli-")); + } } From f4ff5f89f73f987082a4d4f7e6b721e11e725b7a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 14:48:58 +0200 Subject: [PATCH 005/298] i18n for sop version --sop-spec --- sop-java-picocli/src/main/resources/msg_version.properties | 1 + sop-java-picocli/src/main/resources/msg_version_de.properties | 1 + 2 files changed, 2 insertions(+) diff --git a/sop-java-picocli/src/main/resources/msg_version.properties b/sop-java-picocli/src/main/resources/msg_version.properties index c081705..a4c75ca 100644 --- a/sop-java-picocli/src/main/resources/msg_version.properties +++ b/sop-java-picocli/src/main/resources/msg_version.properties @@ -4,6 +4,7 @@ usage.header=Display version information about the tool 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 # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 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 c22d91f..fe460e9 100644 --- a/sop-java-picocli/src/main/resources/msg_version_de.properties +++ b/sop-java-picocli/src/main/resources/msg_version_de.properties @@ -4,6 +4,7 @@ usage.header=Zeige Versionsinformationen über das Programm 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. # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 From 8a66f0bc4f88a1a647471a5a32c5dfe458c4179c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 15:04:51 +0200 Subject: [PATCH 006/298] Implement sop encrypt --profile=XXX --- .../external/operation/EncryptExternal.java | 6 ++++++ .../sop/cli/picocli/commands/EncryptCmd.java | 13 +++++++++++++ .../src/main/java/sop/operation/Encrypt.java | 19 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java index ca8ddf1..1a7208e 100644 --- a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java +++ b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java @@ -85,6 +85,12 @@ public class EncryptExternal implements Encrypt { return this; } + @Override + public Encrypt profile(String profileName) { + commandList.add("--profile=" + profileName); + return this; + } + @Override public Ready plaintext(InputStream plaintext) throws IOException, SOPGPException.KeyIsProtected { diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java index 20b0779..079f9d8 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java @@ -41,6 +41,10 @@ public class EncryptCmd extends AbstractSopCmd { paramLabel = "PASSWORD") List withKeyPassword = new ArrayList<>(); + @CommandLine.Option(names = "--profile", + paramLabel = "PROFILE") + String profile; + @CommandLine.Parameters(index = "0..*", paramLabel = "CERTS") List certs = new ArrayList<>(); @@ -50,6 +54,15 @@ public class EncryptCmd extends AbstractSopCmd { Encrypt encrypt = throwIfUnsupportedSubcommand( SopCLI.getSop().encrypt(), "encrypt"); + if (profile != null) { + try { + encrypt.profile(profile); + } catch (SOPGPException.UnsupportedProfile e) { + String errorMsg = getMsg("sop.error.usage.profile_not_supported", "encrypt", profile); + throw new SOPGPException.UnsupportedProfile(errorMsg, e); + } + } + if (type != null) { try { encrypt.mode(type); diff --git a/sop-java/src/main/java/sop/operation/Encrypt.java b/sop-java/src/main/java/sop/operation/Encrypt.java index 09e5f12..fed1210 100644 --- a/sop-java/src/main/java/sop/operation/Encrypt.java +++ b/sop-java/src/main/java/sop/operation/Encrypt.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; +import sop.Profile; import sop.Ready; import sop.enums.EncryptAs; import sop.exception.SOPGPException; @@ -146,6 +147,24 @@ public interface Encrypt { return withCert(new ByteArrayInputStream(cert)); } + /** + * Pass in a profile. + * + * @param profile profile + * @return builder instance + */ + default Encrypt profile(Profile profile) { + return profile(profile.getName()); + } + + /** + * Pass in a profile identifier. + * + * @param profileName profile identifier + * @return builder instance + */ + Encrypt profile(String profileName); + /** * Encrypt the given data yielding the ciphertext. * @param plaintext plaintext From 90c77706a858114521c545f71989e142e66e58c9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 15:07:43 +0200 Subject: [PATCH 007/298] Add i18n for profile option --- sop-java-picocli/src/main/resources/msg_encrypt.properties | 1 + sop-java-picocli/src/main/resources/msg_encrypt_de.properties | 1 + 2 files changed, 2 insertions(+) diff --git a/sop-java-picocli/src/main/resources/msg_encrypt.properties b/sop-java-picocli/src/main/resources/msg_encrypt.properties index 4b59551..c04ce32 100644 --- a/sop-java-picocli/src/main/resources/msg_encrypt.properties +++ b/sop-java-picocli/src/main/resources/msg_encrypt.properties @@ -7,6 +7,7 @@ as=Type of the input data. Defaults to 'binary' with-password.0=Encrypt the message with a password. with-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) sign-with=Sign the output with a private key +profile=Profile identifier to switch between profiles 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 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 f2fd356..c2ae6ef 100644 --- a/sop-java-picocli/src/main/resources/msg_encrypt_de.properties +++ b/sop-java-picocli/src/main/resources/msg_encrypt_de.properties @@ -7,6 +7,7 @@ as=Format der Nachricht. Standardm with-password.0=Verschlüssle die Nachricht mit einem Passwort with-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). sign-with=Signiere die Nachricht mit einem privaten Schlüssel +profile=Profil-Identifikator um zwischen Profilen zu wechseln with-key-password.0=Passwort zum Entsperren der privaten Schlüssel with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). CERTS[0..*]=Zertifikate für die die Nachricht verschlüsselt werden soll From 8425665fa7f5a954002325d7a68fc72882722fdc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 15:56:52 +0200 Subject: [PATCH 008/298] Expose SOP revision info more fine-grained --- .../external/operation/VersionExternal.java | 36 +++++++++++++ .../src/main/java/sop/operation/Version.java | 53 ++++++++++++++++++- .../sop/testsuite/operation/VersionTest.java | 11 +++- 3 files changed, 96 insertions(+), 4 deletions(-) diff --git a/external-sop/src/main/java/sop/external/operation/VersionExternal.java b/external-sop/src/main/java/sop/external/operation/VersionExternal.java index 95dc8f5..3b21792 100644 --- a/external-sop/src/main/java/sop/external/operation/VersionExternal.java +++ b/external-sop/src/main/java/sop/external/operation/VersionExternal.java @@ -100,6 +100,42 @@ public class VersionExternal implements Version { } } + @Override + public int getSopSpecVersionNumber() { + String revision = getSopSpecVersion(); + String firstLine; + if (revision.contains("\n")) { + firstLine = revision.substring(0, revision.indexOf("\n")); + } else { + firstLine = revision; + } + + if (!firstLine.contains("-")) { + return -1; + } + + return Integer.parseInt(firstLine.substring(firstLine.lastIndexOf("-") + 1)); + } + + @Override + public boolean isSopSpecImplementationIncomplete() { + String revision = getSopSpecVersion(); + return revision.startsWith("~"); + } + + @Override + public String getSopSpecImplementationIncompletenessRemarks() { + String revision = getSopSpecVersion(); + if (revision.contains("\n")) { + String tail = revision.substring(revision.indexOf("\n") + 1).trim(); + + if (!tail.isEmpty()) { + return tail; + } + } + return null; + } + @Override public String getSopSpecVersion() { String[] command = new String[] {binary, "version", "--sop-spec"}; diff --git a/sop-java/src/main/java/sop/operation/Version.java b/sop-java/src/main/java/sop/operation/Version.java index 1cd2698..95b2fe1 100644 --- a/sop-java/src/main/java/sop/operation/Version.java +++ b/sop-java/src/main/java/sop/operation/Version.java @@ -47,13 +47,62 @@ public interface Version { */ String getExtendedVersion(); + /** + * Return the version number of the latest targeted SOP spec revision. + * + * @return SOP spec revision number + */ + int getSopSpecVersionNumber(); + + /** + * Return the latest targeted revision of the SOP spec. + * + * @return SOP spec revision string + */ + default String getSopSpecRevisionString() { + return "draft-dkg-openpgp-stateless-cli-" + String.format("%02d", getSopSpecVersionNumber()); + } + + /** + * Return
true
, if this implementation of the SOP spec is known to be incomplete or defective. + * + * @return true if incomplete, false otherwise + */ + boolean isSopSpecImplementationIncomplete(); + + /** + * Return free-form text containing remarks about the completeness of the SOP implementation. + * If no remarks are known, this method returns
null
. + * + * @return remarks or null + */ + String getSopSpecImplementationIncompletenessRemarks(); + /** * Return the revision of the SOP specification that this implementation is implementing, for example, *
draft-dkg-openpgp-stateless-cli-06
. * If the implementation targets a specific draft but the implementer knows the implementation is incomplete, - * it should prefix the draft title with a "~" (TILDE, U+007E), for example:
~draft-dkg-openpgp-stateless-cli-06
. + * it should prefix the draft title with a "~" (TILDE, U+007E), for example: + *
~draft-dkg-openpgp-stateless-cli-06
. + * The implementation MAY emit additional text about its relationship to the targeted draft on the lines following + * the versioned title. * * @return implemented SOP spec version */ - String getSopSpecVersion(); + default String getSopSpecVersion() { + StringBuilder sb = new StringBuilder(); + if (isSopSpecImplementationIncomplete()) { + sb.append('~'); + } + + sb.append(getSopSpecRevisionString()); + + if (getSopSpecImplementationIncompletenessRemarks() != null) { + sb.append('\n') + .append('\n') + .append(getSopSpecImplementationIncompletenessRemarks()); + } + + return sb.toString(); + } } 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 5ad1a52..8dc958e 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java @@ -14,6 +14,7 @@ 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.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") @@ -56,7 +57,13 @@ public class VersionTest extends AbstractSOPTest { @MethodSource("provideInstances") public void sopSpecVersionTest(SOP sop) { String sopSpec = sop.version().getSopSpecVersion(); - assertTrue(sopSpec.startsWith("draft-dkg-openpgp-stateless-cli-") || - sopSpec.startsWith("~draft-dkg-openpgp-stateless-cli-")); + if (sop.version().isSopSpecImplementationIncomplete()) { + assertTrue(sopSpec.startsWith("~draft-dkg-openpgp-stateless-cli-")); + } else { + assertTrue(sopSpec.startsWith("draft-dkg-openpgp-stateless-cli-")); + } + + int sopRevision = sop.version().getSopSpecVersionNumber(); + assertTrue(sop.version().getSopSpecRevisionString().endsWith("" + sopRevision)); } } From 1a4affde3561cb2914b113b0a7931af9432a507a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 14 Apr 2023 16:29:02 +0200 Subject: [PATCH 009/298] Skip sopSpecVersionTest if --sop-spec is not supported --- .../java/sop/testsuite/operation/VersionTest.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 8dc958e..59208dc 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java @@ -14,8 +14,8 @@ 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.assertThrows; 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 { @@ -56,6 +56,12 @@ public class VersionTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void sopSpecVersionTest(SOP sop) { + try { + sop.version().getSopSpecVersion(); + } catch (RuntimeException e) { + assumeTrue(false); // SOP backend does not support this operation yet + } + String sopSpec = sop.version().getSopSpecVersion(); if (sop.version().isSopSpecImplementationIncomplete()) { assertTrue(sopSpec.startsWith("~draft-dkg-openpgp-stateless-cli-")); From 84a01df4bd97da2f53089cb65ebb6ac9b7238529 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 15:29:06 +0200 Subject: [PATCH 010/298] Rename new methods in Version --- .../external/operation/VersionExternal.java | 4 +- .../src/main/java/sop/operation/Version.java | 69 ++++++++++--------- .../sop/testsuite/operation/VersionTest.java | 4 +- 3 files changed, 39 insertions(+), 38 deletions(-) diff --git a/external-sop/src/main/java/sop/external/operation/VersionExternal.java b/external-sop/src/main/java/sop/external/operation/VersionExternal.java index 3b21792..0b9c5b4 100644 --- a/external-sop/src/main/java/sop/external/operation/VersionExternal.java +++ b/external-sop/src/main/java/sop/external/operation/VersionExternal.java @@ -101,7 +101,7 @@ public class VersionExternal implements Version { } @Override - public int getSopSpecVersionNumber() { + public int getSopSpecRevisionNumber() { String revision = getSopSpecVersion(); String firstLine; if (revision.contains("\n")) { @@ -124,7 +124,7 @@ public class VersionExternal implements Version { } @Override - public String getSopSpecImplementationIncompletenessRemarks() { + public String getSopSpecImplementationRemarks() { String revision = getSopSpecVersion(); if (revision.contains("\n")) { String tail = revision.substring(revision.indexOf("\n") + 1).trim(); diff --git a/sop-java/src/main/java/sop/operation/Version.java b/sop-java/src/main/java/sop/operation/Version.java index 95b2fe1..b6d66b9 100644 --- a/sop-java/src/main/java/sop/operation/Version.java +++ b/sop-java/src/main/java/sop/operation/Version.java @@ -47,37 +47,6 @@ public interface Version { */ String getExtendedVersion(); - /** - * Return the version number of the latest targeted SOP spec revision. - * - * @return SOP spec revision number - */ - int getSopSpecVersionNumber(); - - /** - * Return the latest targeted revision of the SOP spec. - * - * @return SOP spec revision string - */ - default String getSopSpecRevisionString() { - return "draft-dkg-openpgp-stateless-cli-" + String.format("%02d", getSopSpecVersionNumber()); - } - - /** - * Return
true
, if this implementation of the SOP spec is known to be incomplete or defective. - * - * @return true if incomplete, false otherwise - */ - boolean isSopSpecImplementationIncomplete(); - - /** - * Return free-form text containing remarks about the completeness of the SOP implementation. - * If no remarks are known, this method returns
null
. - * - * @return remarks or null - */ - String getSopSpecImplementationIncompletenessRemarks(); - /** * Return the revision of the SOP specification that this implementation is implementing, for example, *
draft-dkg-openpgp-stateless-cli-06
. @@ -95,14 +64,46 @@ public interface Version { sb.append('~'); } - sb.append(getSopSpecRevisionString()); + sb.append(getSopSpecRevisionName()); - if (getSopSpecImplementationIncompletenessRemarks() != null) { + if (getSopSpecImplementationRemarks() != null) { sb.append('\n') .append('\n') - .append(getSopSpecImplementationIncompletenessRemarks()); + .append(getSopSpecImplementationRemarks()); } return sb.toString(); } + + /** + * Return the version number of the latest targeted SOP spec revision. + * + * @return SOP spec revision number + */ + int getSopSpecRevisionNumber(); + + /** + * Return the name of the latest targeted revision of the SOP spec. + * + * @return SOP spec revision string + */ + default String getSopSpecRevisionName() { + return "draft-dkg-openpgp-stateless-cli-" + String.format("%02d", getSopSpecRevisionNumber()); + } + + /** + * Return
true
, if this implementation of the SOP spec is known to be incomplete or defective. + * + * @return true if incomplete, false otherwise + */ + boolean isSopSpecImplementationIncomplete(); + + /** + * Return free-form text containing remarks about the completeness of the SOP implementation. + * If there are no remarks, this method returns
null
. + * + * @return remarks or null + */ + String getSopSpecImplementationRemarks(); + } 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 59208dc..73ba571 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java @@ -69,7 +69,7 @@ public class VersionTest extends AbstractSOPTest { assertTrue(sopSpec.startsWith("draft-dkg-openpgp-stateless-cli-")); } - int sopRevision = sop.version().getSopSpecVersionNumber(); - assertTrue(sop.version().getSopSpecRevisionString().endsWith("" + sopRevision)); + int sopRevision = sop.version().getSopSpecRevisionNumber(); + assertTrue(sop.version().getSopSpecRevisionName().endsWith("" + sopRevision)); } } From 415fc3dd3a02a2d7ec1cd554710ccd1e2e11d1a2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 15:43:01 +0200 Subject: [PATCH 011/298] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bd481e..96d90e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 6.0.0 +- Update implementation to [SOP Specification revision 06](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-06.html). + - Add option `--profile=XYZ` to `encrypt` subcommand + - Add option `--sop-spec` to `version` subcommand + - `Version`: Add different getters for specification-related values + ## 5.0.0 - Update implementation to [SOP Specification revision 05](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html). - Add the concept of profiles From 41260bb02c74c77b2fbb88627e8c9238b0048eab Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 15:45:37 +0200 Subject: [PATCH 012/298] SOP-Java 6.0.0 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 7202f0a..b5ddf19 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '6.0.0' - isSnapshot = true + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 51a7d950e5f385487410162b41bb6179bf1de061 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 17 Apr 2023 15:56:06 +0200 Subject: [PATCH 013/298] SOP-Java 6.0.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index b5ddf19..095db6c 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '6.0.0' - isSnapshot = false + shortVersion = '6.0.1' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From d8cac7b9d7c6fed53c926ba383ac51f471ccadc5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 17:58:47 +0200 Subject: [PATCH 014/298] external-sop: Fix error code mapping of new exceptions --- external-sop/src/main/java/sop/external/ExternalSOP.java | 8 ++++++++ .../java/sop/testsuite/operation/ListProfilesTest.java | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/external-sop/src/main/java/sop/external/ExternalSOP.java b/external-sop/src/main/java/sop/external/ExternalSOP.java index f6e90a5..f22c778 100644 --- a/external-sop/src/main/java/sop/external/ExternalSOP.java +++ b/external-sop/src/main/java/sop/external/ExternalSOP.java @@ -267,6 +267,14 @@ public class ExternalSOP implements SOP { throw new SOPGPException.KeyCannotSign("External SOP backend reported error KeyCannotSign (" + exitCode + "):\n" + errorMessage); + case SOPGPException.IncompatibleOptions.EXIT_CODE: + throw new SOPGPException.IncompatibleOptions("External SOP backend reported error IncompatibleOptions (" + + exitCode + "):\n" + errorMessage); + + case SOPGPException.UnsupportedProfile.EXIT_CODE: + throw new SOPGPException.UnsupportedProfile("External SOP backend reported error UnsupportedProfile (" + + exitCode + "):\n" + errorMessage); + default: throw new RuntimeException("External SOP backend reported unknown exit code (" + exitCode + "):\n" + errorMessage); diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java index e97dd39..af60fc9 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java @@ -9,12 +9,14 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import sop.Profile; import sop.SOP; +import sop.exception.SOPGPException; import java.io.IOException; import java.util.List; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; public class ListProfilesTest extends AbstractSOPTest { @@ -30,4 +32,11 @@ public class ListProfilesTest extends AbstractSOPTest { assertFalse(profiles.isEmpty()); } + @ParameterizedTest + @MethodSource("provideInstances") + public void listUnsupportedProfiles(SOP sop) throws IOException { + assertThrows(SOPGPException.UnsupportedProfile.class, () -> sop + .listProfiles() + .subcommand("invalid")); + } } From 78ecf2f554c91ae6ffcfc0b34b369b883b8582dc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 18:04:23 +0200 Subject: [PATCH 015/298] ListProfiles: Add shortcut methods generateKey() and encrypt() --- .../main/java/sop/operation/ListProfiles.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/sop-java/src/main/java/sop/operation/ListProfiles.java b/sop-java/src/main/java/sop/operation/ListProfiles.java index 5514649..0c17bd6 100644 --- a/sop-java/src/main/java/sop/operation/ListProfiles.java +++ b/sop-java/src/main/java/sop/operation/ListProfiles.java @@ -22,4 +22,22 @@ public interface ListProfiles { */ List subcommand(String command); + /** + * Return a list of {@link Profile Profiles} supported by the {@link GenerateKey} implementation. + * + * @return profiles + */ + default List generateKey() { + return subcommand("generate-key"); + } + + /** + * Return a list of {@link Profile Profiles} supported by the {@link Encrypt} implementation. + * + * @return profiles + */ + default List encrypt() { + return subcommand("encrypt"); + } + } From 4a7c2b74da19d032f378879af96a521cb7bb8f22 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 18:05:10 +0200 Subject: [PATCH 016/298] Add test for listing encrypt profiles, use new shortcut methods --- .../testsuite/operation/ListProfilesTest.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java index af60fc9..f566dd3 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java @@ -27,8 +27,20 @@ public class ListProfilesTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void listGenerateKeyProfiles(SOP sop) throws IOException { - List profiles = sop.listProfiles() - .subcommand("generate-key"); + List profiles = sop + .listProfiles() + .generateKey(); + + assertFalse(profiles.isEmpty()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void listEncryptProfiles(SOP sop) throws IOException { + List profiles = sop + .listProfiles() + .encrypt(); + assertFalse(profiles.isEmpty()); } From bb2b4e03fbcf38b333b298affafc9e109027d62b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 18 Apr 2023 18:15:11 +0200 Subject: [PATCH 017/298] Add comment to remind future self of adding new exception types to switch-case --- external-sop/src/main/java/sop/external/ExternalSOP.java | 1 + 1 file changed, 1 insertion(+) diff --git a/external-sop/src/main/java/sop/external/ExternalSOP.java b/external-sop/src/main/java/sop/external/ExternalSOP.java index f22c778..bc5eb46 100644 --- a/external-sop/src/main/java/sop/external/ExternalSOP.java +++ b/external-sop/src/main/java/sop/external/ExternalSOP.java @@ -276,6 +276,7 @@ public class ExternalSOP implements SOP { exitCode + "):\n" + errorMessage); default: + // Did you forget to add a case for a new exception type? throw new RuntimeException("External SOP backend reported unknown exit code (" + exitCode + "):\n" + errorMessage); } From aeda534f379d1e2ce4867c6de72829572a4fde23 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Apr 2023 15:52:53 +0200 Subject: [PATCH 018/298] Add DSL for testing verification results --- .../test/java/sop/util/VerificationTest.java | 39 +++++++- .../java/sop/testsuite/JUtils.java | 65 +------------ .../assertions/VerificationAssert.java | 71 ++++++++++++++ .../assertions/VerificationListAssert.java | 70 ++++++++++++++ .../testsuite/assertions/package-info.java | 8 ++ .../DetachedSignDetachedVerifyTest.java | 96 +++++++++++++++---- .../operation/EncryptDecryptTest.java | 29 ++++-- .../operation/InlineSignInlineVerifyTest.java | 43 +++++++-- 8 files changed, 326 insertions(+), 95 deletions(-) create mode 100644 sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationAssert.java create mode 100644 sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationListAssert.java create mode 100644 sop-java/src/testFixtures/java/sop/testsuite/assertions/package-info.java diff --git a/sop-java/src/test/java/sop/util/VerificationTest.java b/sop-java/src/test/java/sop/util/VerificationTest.java index 3688d6d..b292688 100644 --- a/sop-java/src/test/java/sop/util/VerificationTest.java +++ b/sop-java/src/test/java/sop/util/VerificationTest.java @@ -7,6 +7,7 @@ package sop.util; import org.junit.jupiter.api.Test; import sop.Verification; import sop.enums.SignatureMode; +import sop.testsuite.assertions.VerificationAssert; import java.util.Date; @@ -21,12 +22,37 @@ public class VerificationTest { String certFP = "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B"; Verification verification = new Verification(signDate, keyFP, certFP); assertEquals("2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B", verification.toString()); + + VerificationAssert.assertThatVerification(verification) + .issuedBy(certFP) + .isBySigningKey(keyFP) + .isCreatedAt(signDate) + .hasMode(null) + .hasDescription(null); } + @Test public void limitedParsingTest() { String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B"; Verification verification = Verification.fromString(string); assertEquals(string, verification.toString()); + VerificationAssert.assertThatVerification(verification) + .isCreatedAt(UTCUtil.parseUTCDate("2022-11-07T15:01:24Z")) + .issuedBy("F9E6F53F7201C60A87064EAB0B27F2B0760A1209", "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B") + .hasMode(null) + .hasDescription(null); + } + + @Test + public void parsingWithModeTest() { + String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B mode:text"; + Verification verification = Verification.fromString(string); + assertEquals(string, verification.toString()); + VerificationAssert.assertThatVerification(verification) + .isCreatedAt(UTCUtil.parseUTCDate("2022-11-07T15:01:24Z")) + .issuedBy("F9E6F53F7201C60A87064EAB0B27F2B0760A1209", "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B") + .hasMode(SignatureMode.text) + .hasDescription(null); } @Test @@ -37,9 +63,13 @@ public class VerificationTest { SignatureMode mode = SignatureMode.binary; String description = "certificate from dkg.asc"; Verification verification = new Verification(signDate, keyFP, certFP, mode, description); + assertEquals("2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B mode:binary certificate from dkg.asc", verification.toString()); - assertEquals(SignatureMode.binary, verification.getSignatureMode()); - assertEquals(description, verification.getDescription()); + VerificationAssert.assertThatVerification(verification) + .isCreatedAt(signDate) + .issuedBy(keyFP, certFP) + .hasMode(SignatureMode.binary) + .hasDescription(description); } @Test @@ -52,5 +82,10 @@ public class VerificationTest { string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B certificate from dkg.asc"; verification = Verification.fromString(string); assertEquals(string, verification.toString()); + VerificationAssert.assertThatVerification(verification) + .isCreatedAt(UTCUtil.parseUTCDate("2022-11-07T15:01:24Z")) + .issuedBy("F9E6F53F7201C60A87064EAB0B27F2B0760A1209", "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B") + .hasMode(null) + .hasDescription("certificate from dkg.asc"); } } diff --git a/sop-java/src/testFixtures/java/sop/testsuite/JUtils.java b/sop-java/src/testFixtures/java/sop/testsuite/JUtils.java index c30d86b..5302192 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/JUtils.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/JUtils.java @@ -4,15 +4,14 @@ package sop.testsuite; -import sop.Verification; import sop.util.UTCUtil; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Date; -import java.util.List; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; public class JUtils { @@ -157,66 +156,12 @@ public class JUtils { return string.getBytes(StandardCharsets.UTF_8); } - public static void assertSignedBy(List verifications, String primaryFingerprint) { - for (Verification verification : verifications) { - if (verification.getSigningCertFingerprint().equals(primaryFingerprint)) { - return; - } - } - - if (verifications.isEmpty()) { - fail("Verification list is empty."); - } - - fail("Verification list does not contain verification by cert " + primaryFingerprint + ":\n" + - Arrays.toString(verifications.toArray(new Verification[0]))); + public static void assertDateEquals(Date expected, Date actual) { + assertEquals(UTCUtil.formatUTCDate(expected), UTCUtil.formatUTCDate(actual)); } - public static void assertSignedBy(List verifications, String signingFingerprint, String primaryFingerprint) { - for (Verification verification : verifications) { - if (verification.getSigningCertFingerprint().equals(primaryFingerprint) && verification.getSigningKeyFingerprint().equals(signingFingerprint)) { - return; - } - } - - if (verifications.isEmpty()) { - fail("Verification list is empty."); - } - - fail("Verification list does not contain verification by key " + signingFingerprint + " on cert " + primaryFingerprint + ":\n" + - Arrays.toString(verifications.toArray(new Verification[0]))); + public static boolean dateEquals(Date expected, Date actual) { + return UTCUtil.formatUTCDate(expected).equals(UTCUtil.formatUTCDate(actual)); } - public static void assertSignedBy(List verifications, String primaryFingerprint, Date signatureDate) { - for (Verification verification : verifications) { - if (verification.getSigningCertFingerprint().equals(primaryFingerprint) && - verification.getCreationTime().equals(signatureDate)) { - return; - } - } - - if (verifications.isEmpty()) { - fail("Verification list is empty."); - } - - fail("Verification list does not contain verification by cert " + primaryFingerprint + " made at " + UTCUtil.formatUTCDate(signatureDate) + ":\n" + - Arrays.toString(verifications.toArray(new Verification[0]))); - } - - public static void assertSignedBy(List verifications, String signingFingerprint, String primaryFingerprint, Date signatureDate) { - for (Verification verification : verifications) { - if (verification.getSigningCertFingerprint().equals(primaryFingerprint) && - verification.getSigningKeyFingerprint().equals(signingFingerprint) && - verification.getCreationTime().equals(signatureDate)) { - return; - } - } - - if (verifications.isEmpty()) { - fail("Verification list is empty."); - } - - fail("Verification list does not contain verification by key" + signingFingerprint + " on cert " + primaryFingerprint + " made at " + UTCUtil.formatUTCDate(signatureDate) + ":\n" + - Arrays.toString(verifications.toArray(new Verification[0]))); - } } diff --git a/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationAssert.java b/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationAssert.java new file mode 100644 index 0000000..35e3ed6 --- /dev/null +++ b/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationAssert.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.assertions; + +import sop.Verification; +import sop.enums.SignatureMode; +import sop.testsuite.JUtils; + +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public final class VerificationAssert { + + private final Verification verification; + + public static VerificationAssert assertThatVerification(Verification verification) { + return new VerificationAssert(verification); + } + + private VerificationAssert(Verification verification) { + this.verification = verification; + } + + public VerificationAssert issuedBy(String signingKeyFingerprint, String primaryFingerprint) { + return isBySigningKey(signingKeyFingerprint) + .issuedBy(primaryFingerprint); + } + + public VerificationAssert issuedBy(String primaryFingerprint) { + assertEquals(primaryFingerprint, verification.getSigningCertFingerprint()); + return this; + } + + public VerificationAssert isBySigningKey(String signingKeyFingerprint) { + assertEquals(signingKeyFingerprint, verification.getSigningKeyFingerprint()); + return this; + } + + public VerificationAssert isCreatedAt(Date creationDate) { + JUtils.assertDateEquals(creationDate, verification.getCreationTime()); + return this; + } + + public VerificationAssert hasDescription(String description) { + assertEquals(description, verification.getDescription()); + return this; + } + + public VerificationAssert hasDescriptionOrNull(String description) { + if (verification.getDescription() == null) { + return this; + } + + return hasDescription(description); + } + + public VerificationAssert hasMode(SignatureMode mode) { + assertEquals(mode, verification.getSignatureMode()); + return this; + } + + public VerificationAssert hasModeOrNull(SignatureMode mode) { + if (verification.getSignatureMode() == null) { + return this; + } + return hasMode(mode); + } +} diff --git a/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationListAssert.java b/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationListAssert.java new file mode 100644 index 0000000..6c90609 --- /dev/null +++ b/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationListAssert.java @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.assertions; + +import sop.Verification; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public final class VerificationListAssert { + + private final List verificationList = new ArrayList<>(); + + private VerificationListAssert(List verifications) { + this.verificationList.addAll(verifications); + } + + public static VerificationListAssert assertThatVerificationList(List verifications) { + return new VerificationListAssert(verifications); + } + + public VerificationListAssert isEmpty() { + assertTrue(verificationList.isEmpty()); + return this; + } + + public VerificationListAssert isNotEmpty() { + assertFalse(verificationList.isEmpty()); + return this; + } + + public VerificationListAssert sizeEquals(int size) { + assertEquals(size, verificationList.size()); + return this; + } + + public VerificationAssert hasSingleItem() { + sizeEquals(1); + return VerificationAssert.assertThatVerification(verificationList.get(0)); + } + + public VerificationListAssert containsVerificationByCert(String primaryFingerprint) { + for (Verification verification : verificationList) { + if (primaryFingerprint.equals(verification.getSigningCertFingerprint())) { + return this; + } + } + fail("No verification was issued by certificate " + primaryFingerprint); + return this; + } + + public VerificationListAssert containsVerificationBy(String signingKeyFingerprint, String primaryFingerprint) { + for (Verification verification : verificationList) { + if (primaryFingerprint.equals(verification.getSigningCertFingerprint()) && + signingKeyFingerprint.equals(verification.getSigningKeyFingerprint())) { + return this; + } + } + + fail("No verification was issued by key " + signingKeyFingerprint + " of cert " + primaryFingerprint); + return this; + } +} diff --git a/sop-java/src/testFixtures/java/sop/testsuite/assertions/package-info.java b/sop-java/src/testFixtures/java/sop/testsuite/assertions/package-info.java new file mode 100644 index 0000000..8e2ef7c --- /dev/null +++ b/sop-java/src/testFixtures/java/sop/testsuite/assertions/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * DSL for assertions on SOP objects. + */ +package sop.testsuite.assertions; diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java index 06a0f9b..e715c14 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java @@ -11,9 +11,11 @@ import org.junit.jupiter.params.provider.MethodSource; import sop.SOP; import sop.Verification; import sop.enums.SignAs; +import sop.enums.SignatureMode; import sop.exception.SOPGPException; import sop.testsuite.JUtils; import sop.testsuite.TestData; +import sop.testsuite.assertions.VerificationListAssert; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -21,7 +23,6 @@ import java.util.Date; import java.util.List; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") @@ -47,8 +48,11 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { .signatures(signature) .data(message); - assertFalse(verificationList.isEmpty()); - JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT) + .hasModeOrNull(SignatureMode.binary); } @ParameterizedTest @@ -68,8 +72,11 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { .signatures(signature) .data(message); - assertFalse(verificationList.isEmpty()); - JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT) + .hasModeOrNull(SignatureMode.text); } @ParameterizedTest @@ -83,8 +90,10 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { .signatures(signature) .data(message); - assertFalse(verificationList.isEmpty()); - JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT, TestData.ALICE_DETACHED_SIGNED_MESSAGE_DATE); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT); } @ParameterizedTest @@ -103,8 +112,10 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { .signatures(signature) .data(message); - assertFalse(verificationList.isEmpty()); - JUtils.assertSignedBy(verificationList, TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT); } @ParameterizedTest @@ -123,8 +134,10 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { .signatures(signature) .data(message); - assertFalse(verificationList.isEmpty()); - JUtils.assertSignedBy(verificationList, TestData.CAROL_SIGNING_FINGERPRINT, TestData.CAROL_PRIMARY_FINGERPRINT); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.CAROL_SIGNING_FINGERPRINT, TestData.CAROL_PRIMARY_FINGERPRINT); } @ParameterizedTest @@ -146,7 +159,10 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { .signatures(signature) .data(message); - assertFalse(verificationList.isEmpty()); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.PASSWORD_PROTECTED_SIGNING_FINGERPRINT, TestData.PASSWORD_PROTECTED_PRIMARY_FINGERPRINT); } @ParameterizedTest @@ -170,8 +186,10 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { .signatures(armored) .data(message); - assertFalse(verificationList.isEmpty()); - JUtils.assertSignedBy(verificationList, TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT); } @ParameterizedTest @@ -202,6 +220,21 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { .data(message)); } + @ParameterizedTest + @MethodSource("provideInstances") + public void signWithAliceVerifyWithBobThrowsNoSignature(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + byte[] signatures = sop.detachedSign() + .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .data(message) + .toByteArrayAndResult() + .getBytes(); + + assertThrows(SOPGPException.NoSignature.class, () -> sop.detachedVerify() + .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) + .signatures(signatures) + .data(message)); + } @ParameterizedTest @MethodSource("provideInstances") @@ -229,11 +262,15 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { .toByteArrayAndResult() .getBytes(); - assertFalse(sop.verify() + List verificationList = sop.verify() .cert(TestData.PASSWORD_PROTECTED_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signature) - .data(message) - .isEmpty()); + .data(message); + + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.PASSWORD_PROTECTED_SIGNING_FINGERPRINT, TestData.PASSWORD_PROTECTED_PRIMARY_FINGERPRINT); } @ParameterizedTest @@ -247,4 +284,29 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { .data(message)); } + @ParameterizedTest + @MethodSource("provideInstances") + public void signVerifyWithMultipleKeys(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + byte[] signatures = sop.detachedSign() + .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) + .data(message) + .toByteArrayAndResult() + .getBytes(); + + List verificationList = sop.detachedVerify() + .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) + .signatures(signatures) + .data(message); + + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .sizeEquals(2) + .containsVerificationBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT) + .containsVerificationBy(TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT); + } + + } 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 8c62fb5..23f117e 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java @@ -13,9 +13,10 @@ import sop.DecryptionResult; import sop.SOP; import sop.Verification; import sop.enums.EncryptAs; +import sop.enums.SignatureMode; import sop.exception.SOPGPException; -import sop.testsuite.JUtils; import sop.testsuite.TestData; +import sop.testsuite.assertions.VerificationListAssert; import sop.util.UTCUtil; import java.io.IOException; @@ -25,8 +26,6 @@ import java.util.List; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -142,6 +141,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] ciphertext = sop.encrypt() .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .mode(EncryptAs.Binary) .plaintext(message) .getBytes(); @@ -156,9 +156,13 @@ public class EncryptDecryptTest extends AbstractSOPTest { DecryptionResult result = bytesAndResult.getResult(); assertNotNull(result.getSessionKey().get()); + List verificationList = result.getVerifications(); - assertEquals(1, verificationList.size()); - JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT) + .hasModeOrNull(SignatureMode.binary); } @ParameterizedTest @@ -183,9 +187,12 @@ public class EncryptDecryptTest extends AbstractSOPTest { DecryptionResult result = bytesAndResult.getResult(); assertNotNull(result.getSessionKey().get()); + List verificationList = result.getVerifications(); - assertEquals(1, verificationList.size()); - JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT) + .hasModeOrNull(SignatureMode.text); } @ParameterizedTest @@ -216,8 +223,10 @@ public class EncryptDecryptTest extends AbstractSOPTest { .ciphertext(ciphertext) .toByteArrayAndResult(); - assertFalse(bytesAndResult.getResult().getVerifications().isEmpty()); - assertArrayEquals(message, bytesAndResult.getBytes()); + List verifications = bytesAndResult.getResult().getVerifications(); + VerificationListAssert.assertThatVerificationList(verifications) + .isNotEmpty() + .hasSingleItem(); } @ParameterizedTest @@ -247,6 +256,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { .ciphertext(message) .toByteArrayAndResult(); + // Some implementations do not throw NoSignature and instead return an empty list. if (bytesAndResult.getResult().getVerifications().isEmpty()) { throw new SOPGPException.NoSignature("No verifiable signature found."); } @@ -280,6 +290,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { .ciphertext(message) .toByteArrayAndResult(); + // Some implementations do not throw NoSignature and instead return an empty list. if (bytesAndResult.getResult().getVerifications().isEmpty()) { throw new SOPGPException.NoSignature("No verifiable signature found."); } diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java index 7d0c227..39a26c6 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java @@ -13,9 +13,11 @@ import sop.ByteArrayAndResult; import sop.SOP; import sop.Verification; import sop.enums.InlineSignAs; +import sop.enums.SignatureMode; import sop.exception.SOPGPException; import sop.testsuite.JUtils; import sop.testsuite.TestData; +import sop.testsuite.assertions.VerificationListAssert; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -51,8 +53,12 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { .toByteArrayAndResult(); assertArrayEquals(message, bytesAndResult.getBytes()); + List verificationList = bytesAndResult.getResult(); - JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT); } @ParameterizedTest @@ -74,8 +80,12 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { .toByteArrayAndResult(); assertArrayEquals(message, bytesAndResult.getBytes()); + List verificationList = bytesAndResult.getResult(); - JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT); } @ParameterizedTest @@ -97,8 +107,12 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { .toByteArrayAndResult(); assertArrayEquals(message, bytesAndResult.getBytes()); + List verificationList = bytesAndResult.getResult(); - JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT) + .hasModeOrNull(SignatureMode.text); } @ParameterizedTest @@ -111,8 +125,13 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult(); + List verificationList = bytesAndResult.getResult(); - JUtils.assertSignedBy(verificationList, TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT, signatureDate); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .isCreatedAt(signatureDate) + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT); } @ParameterizedTest @@ -161,8 +180,12 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { .toByteArrayAndResult(); assertArrayEquals(message, bytesAndResult.getBytes()); + List verificationList = bytesAndResult.getResult(); - JUtils.assertSignedBy(verificationList, TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT); } @ParameterizedTest @@ -183,8 +206,12 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { .toByteArrayAndResult(); assertArrayEquals(message, bytesAndResult.getBytes()); + List verificationList = bytesAndResult.getResult(); - JUtils.assertSignedBy(verificationList, TestData.CAROL_SIGNING_FINGERPRINT, TestData.CAROL_PRIMARY_FINGERPRINT); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.CAROL_SIGNING_FINGERPRINT, TestData.CAROL_PRIMARY_FINGERPRINT); } @ParameterizedTest @@ -205,7 +232,9 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { .toByteArrayAndResult(); List verificationList = bytesAndResult.getResult(); - JUtils.assertSignedBy(verificationList, TestData.PASSWORD_PROTECTED_SIGNING_FINGERPRINT, TestData.PASSWORD_PROTECTED_PRIMARY_FINGERPRINT); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .issuedBy(TestData.PASSWORD_PROTECTED_SIGNING_FINGERPRINT, TestData.PASSWORD_PROTECTED_PRIMARY_FINGERPRINT); } } From a722e98578b805e37f23be0a3984797e05e13f0d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Apr 2023 16:12:32 +0200 Subject: [PATCH 019/298] Update changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96d90e4..0a4e918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 6.0.1-SNAPSHOT +- `listProfiles()`: Add shortcut methods `generateKey()` and `encrypt()` +- Add DSL for testing `Verification` results +- `external-sop`: Properly map error codes to new exception types: + - `UNSUPPORTED_PROFILE` + - `INCOMPATIBLE_OPTIONS` + +## 5.0.1 +- `external-sop`: Properly map error codes to new exception types: + - `UNSUPPORTED_PROFILE` + - `INCOMPATIBLE_OPTIONS` + ## 6.0.0 - Update implementation to [SOP Specification revision 06](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-06.html). - Add option `--profile=XYZ` to `encrypt` subcommand From 0fccf3051ccc1ee22d6a15ea444eaa28c973ad2e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Apr 2023 16:21:37 +0200 Subject: [PATCH 020/298] Refactor AbstractSOPTest --- .../testsuite/operation/AbstractSOPTest.java | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) 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 fa9cbbf..8595898 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.params.provider.Arguments; import sop.SOP; import sop.testsuite.SOPInstanceFactory; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -19,24 +20,29 @@ public abstract class AbstractSOPTest { private static final List backends = new ArrayList<>(); static { - // populate instances list via configured test subject factory - String factoryName = System.getenv("test.implementation"); - if (factoryName != null) { - try { - Class testSubjectFactoryClass = Class.forName(factoryName); - SOPInstanceFactory factory = (SOPInstanceFactory) testSubjectFactoryClass.newInstance(); - Map testSubjects = factory.provideSOPInstances(); + initBackends(); + } - for (String key : testSubjects.keySet()) { - backends.add(Arguments.of(Named.of(key, testSubjects.get(key)))); - } - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } catch (InstantiationException e) { - throw new RuntimeException(e); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } + // populate instances list via configured test subject factory + private static void initBackends() { + String factoryName = System.getenv("test.implementation"); + if (factoryName == null) { + return; + } + + SOPInstanceFactory factory; + try { + Class testSubjectFactoryClass = Class.forName(factoryName); + factory = (SOPInstanceFactory) testSubjectFactoryClass + .getDeclaredConstructor().newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | + InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + + Map testSubjects = factory.provideSOPInstances(); + for (String key : testSubjects.keySet()) { + backends.add(Arguments.of(Named.of(key, testSubjects.get(key)))); } } From 790d80ec29340b337d0606ae6c81f0be351d11b5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Apr 2023 16:22:53 +0200 Subject: [PATCH 021/298] DateParsingTest: make armor command final --- .../src/test/java/sop/cli/picocli/DateParsingTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/DateParsingTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/DateParsingTest.java index 1fff1d5..e4550c7 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/DateParsingTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/DateParsingTest.java @@ -14,7 +14,7 @@ import sop.cli.picocli.commands.ArmorCmd; import sop.util.UTCUtil; public class DateParsingTest { - private AbstractSopCmd cmd = new ArmorCmd(); // we use ArmorCmd as a concrete implementation. + private final AbstractSopCmd cmd = new ArmorCmd(); // we use ArmorCmd as a concrete implementation. @Test public void parseNotAfterDashReturnsEndOfTime() { From 0aabfac695db1d17bc401406e2aec4b05b7bb9d2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Apr 2023 16:23:18 +0200 Subject: [PATCH 022/298] DetachedVerifyExternal: Make certs set final --- .../java/sop/external/operation/DetachedVerifyExternal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java b/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java index 05318d4..b9cd182 100644 --- a/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java +++ b/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java @@ -31,7 +31,7 @@ public class DetachedVerifyExternal implements DetachedVerify { private final List commandList = new ArrayList<>(); private final List envList; - private Set certs = new HashSet<>(); + private final Set certs = new HashSet<>(); private InputStream signatures; private int certCounter = 0; From ed59c713eb5b20a7610503a6a94da9f9b1f4c504 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Apr 2023 16:28:04 +0200 Subject: [PATCH 023/298] Remove unused 'throws IOException' declarations --- .../main/java/sop/external/operation/ArmorExternal.java | 5 ++--- .../main/java/sop/external/operation/DearmorExternal.java | 3 +-- .../sop/external/operation/DetachedVerifyExternal.java | 4 ++-- .../main/java/sop/external/operation/EncryptExternal.java | 2 +- .../java/sop/external/operation/InlineSignExternal.java | 2 +- .../java/sop/testsuite/operation/ListProfilesTest.java | 7 +++---- 6 files changed, 10 insertions(+), 13 deletions(-) diff --git a/external-sop/src/main/java/sop/external/operation/ArmorExternal.java b/external-sop/src/main/java/sop/external/operation/ArmorExternal.java index cb53573..4df7fca 100644 --- a/external-sop/src/main/java/sop/external/operation/ArmorExternal.java +++ b/external-sop/src/main/java/sop/external/operation/ArmorExternal.java @@ -10,11 +10,10 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.Armor; -import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import java.util.Properties; import java.util.List; +import java.util.Properties; /** * Implementation of the {@link Armor} operation using an external SOP binary. @@ -37,7 +36,7 @@ public class ArmorExternal implements Armor { } @Override - public Ready data(InputStream data) throws SOPGPException.BadData, IOException { + public Ready data(InputStream data) throws SOPGPException.BadData { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data); } } diff --git a/external-sop/src/main/java/sop/external/operation/DearmorExternal.java b/external-sop/src/main/java/sop/external/operation/DearmorExternal.java index 7fa1fdc..bedf018 100644 --- a/external-sop/src/main/java/sop/external/operation/DearmorExternal.java +++ b/external-sop/src/main/java/sop/external/operation/DearmorExternal.java @@ -9,7 +9,6 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.Dearmor; -import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -30,7 +29,7 @@ public class DearmorExternal implements Dearmor { } @Override - public Ready data(InputStream data) throws SOPGPException.BadData, IOException { + public Ready data(InputStream data) throws SOPGPException.BadData { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data); } } diff --git a/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java b/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java index b9cd182..2f19c5b 100644 --- a/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java +++ b/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java @@ -54,13 +54,13 @@ public class DetachedVerifyExternal implements DetachedVerify { } @Override - public DetachedVerify cert(InputStream cert) throws SOPGPException.BadData, IOException { + public DetachedVerify cert(InputStream cert) throws SOPGPException.BadData { this.certs.add(cert); return this; } @Override - public VerifySignatures signatures(InputStream signatures) throws SOPGPException.BadData, IOException { + public VerifySignatures signatures(InputStream signatures) throws SOPGPException.BadData { this.signatures = signatures; return this; } diff --git a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java index 1a7208e..bc40208 100644 --- a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java +++ b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java @@ -93,7 +93,7 @@ public class EncryptExternal implements Encrypt { @Override public Ready plaintext(InputStream plaintext) - throws IOException, SOPGPException.KeyIsProtected { + throws SOPGPException.KeyIsProtected { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, plaintext); } } diff --git a/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java b/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java index d78dd7b..68a630d 100644 --- a/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java +++ b/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java @@ -62,7 +62,7 @@ public class InlineSignExternal implements InlineSign { } @Override - public Ready data(InputStream data) throws IOException, SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { + public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data); } } diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java index f566dd3..6d3c4c4 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java @@ -11,7 +11,6 @@ import sop.Profile; import sop.SOP; import sop.exception.SOPGPException; -import java.io.IOException; import java.util.List; import java.util.stream.Stream; @@ -26,7 +25,7 @@ public class ListProfilesTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") - public void listGenerateKeyProfiles(SOP sop) throws IOException { + public void listGenerateKeyProfiles(SOP sop) { List profiles = sop .listProfiles() .generateKey(); @@ -36,7 +35,7 @@ public class ListProfilesTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") - public void listEncryptProfiles(SOP sop) throws IOException { + public void listEncryptProfiles(SOP sop) { List profiles = sop .listProfiles() .encrypt(); @@ -46,7 +45,7 @@ public class ListProfilesTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") - public void listUnsupportedProfiles(SOP sop) throws IOException { + public void listUnsupportedProfiles(SOP sop) { assertThrows(SOPGPException.UnsupportedProfile.class, () -> sop .listProfiles() .subcommand("invalid")); From e336e536a8dc7cc797c26c333c7b0380feb14a64 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Apr 2023 16:44:49 +0200 Subject: [PATCH 024/298] Javadoc: Insert

tags to preserve newlines --- sop-java/src/main/java/sop/SOP.java | 12 ++++++------ sop-java/src/main/java/sop/SigningResult.java | 2 +- .../src/main/java/sop/exception/SOPGPException.java | 2 +- .../src/main/java/sop/util/ProxyOutputStream.java | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sop-java/src/main/java/sop/SOP.java b/sop-java/src/main/java/sop/SOP.java index c74c6cd..804cd28 100644 --- a/sop-java/src/main/java/sop/SOP.java +++ b/sop-java/src/main/java/sop/SOP.java @@ -54,7 +54,7 @@ public interface SOP { /** * Create detached signatures. * Customize the operation using the builder {@link DetachedSign}. - * + *

* If you want to sign a message inline, use {@link #inlineSign()} instead. * * @return builder instance @@ -66,7 +66,7 @@ public interface SOP { /** * Create detached signatures. * Customize the operation using the builder {@link DetachedSign}. - * + *

* If you want to sign a message inline, use {@link #inlineSign()} instead. * * @return builder instance @@ -75,7 +75,7 @@ public interface SOP { /** * Sign a message using inline signatures. - * + *

* If you need to create detached signatures, use {@link #detachedSign()} instead. * * @return builder instance @@ -85,7 +85,7 @@ public interface SOP { /** * Verify detached signatures. * Customize the operation using the builder {@link DetachedVerify}. - * + *

* If you need to verify an inline-signed message, use {@link #inlineVerify()} instead. * * @return builder instance @@ -97,7 +97,7 @@ public interface SOP { /** * Verify detached signatures. * Customize the operation using the builder {@link DetachedVerify}. - * + *

* If you need to verify an inline-signed message, use {@link #inlineVerify()} instead. * * @return builder instance @@ -106,7 +106,7 @@ public interface SOP { /** * Verify signatures of an inline-signed message. - * + *

* If you need to verify detached signatures over a message, use {@link #detachedVerify()} instead. * * @return builder instance diff --git a/sop-java/src/main/java/sop/SigningResult.java b/sop-java/src/main/java/sop/SigningResult.java index 2cb142d..1ea1ba8 100644 --- a/sop-java/src/main/java/sop/SigningResult.java +++ b/sop-java/src/main/java/sop/SigningResult.java @@ -19,7 +19,7 @@ public final class SigningResult { * Return a 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. * diff --git a/sop-java/src/main/java/sop/exception/SOPGPException.java b/sop-java/src/main/java/sop/exception/SOPGPException.java index f044fa6..1d13065 100644 --- a/sop-java/src/main/java/sop/exception/SOPGPException.java +++ b/sop-java/src/main/java/sop/exception/SOPGPException.java @@ -337,7 +337,7 @@ public abstract class SOPGPException extends RuntimeException { /** * Exception that gets thrown if a special designator (starting with @) is given, but the filesystem contains * a file matching the designator. - * + *

* E.g.

@ENV:FOO
is given, but
./@ENV:FOO
exists on the filesystem. */ public static class AmbiguousInput extends SOPGPException { diff --git a/sop-java/src/main/java/sop/util/ProxyOutputStream.java b/sop-java/src/main/java/sop/util/ProxyOutputStream.java index 0559e8f..ed24fc2 100644 --- a/sop-java/src/main/java/sop/util/ProxyOutputStream.java +++ b/sop-java/src/main/java/sop/util/ProxyOutputStream.java @@ -12,7 +12,7 @@ import java.io.OutputStream; * {@link OutputStream} that buffers data being written into it, until its underlying output stream is being replaced. * At that point, first all the buffered data is being written to the underlying stream, followed by any successive * data that may get written to the {@link ProxyOutputStream}. - * + *

* This class is useful if we need to provide an {@link OutputStream} at one point in time when the final * target output stream is not yet known. */ From 226b5d99a050332426c3f3ff5cc6448c6b13e90c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 26 Apr 2023 16:47:53 +0200 Subject: [PATCH 025/298] Fix issues with i18n properties --- .../src/main/resources/msg_decrypt.properties | 4 ++-- .../src/main/resources/msg_decrypt_de.properties | 2 +- .../src/main/resources/msg_detached-sign.properties | 4 ++-- .../src/main/resources/msg_detached-verify.properties | 8 ++++---- .../src/main/resources/msg_encrypt.properties | 2 +- .../src/main/resources/msg_inline-sign.properties | 6 +++--- .../src/main/resources/msg_inline-sign_de.properties | 2 +- sop-java-picocli/src/main/resources/msg_sop.properties | 2 +- .../src/main/resources/msg_version_de.properties | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/sop-java-picocli/src/main/resources/msg_decrypt.properties b/sop-java-picocli/src/main/resources/msg_decrypt.properties index 47d1c31..c68a1b8 100644 --- a/sop-java-picocli/src/main/resources/msg_decrypt.properties +++ b/sop-java-picocli/src/main/resources/msg_decrypt.properties @@ -5,10 +5,10 @@ usage.header=Decrypt a message from standard input session-key-out=Can be used to learn the session key on successful decryption with-session-key.0=Symmetric message key (session key). with-session-key.1=Enables decryption of the "CIPHERTEXT" using the session key directly against the "SEIPD" packet. -with-session-key.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) +with-session-key.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). with-password.0=Symmetric passphrase to decrypt the message with. with-password.1=Enables decryption based on any "SKESK" packets in the "CIPHERTEXT". -with-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) +with-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). verify-out=Emits signature verification status to the designated output verify-with=Certificates for signature verification not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) 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 0ef4a4c..249e39c 100644 --- a/sop-java-picocli/src/main/resources/msg_decrypt_de.properties +++ b/sop-java-picocli/src/main/resources/msg_decrypt_de.properties @@ -5,7 +5,7 @@ usage.header=Entschl session-key-out=Extrahiere den Nachrichtenschlüssel nach erfolgreicher Entschlüsselung with-session-key.0=Symmetrischer Nachrichtenschlüssel (Sitzungsschlüssel). with-session-key.1=Ermöglicht direkte Entschlüsselung des im "CIPHERTEXT" enhaltenen "SEIPD" Paketes mithilfe des Nachrichtenschlüssels. -with_session-key.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +with-session-key.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). with-password.0=Symmetrisches Passwort zur Entschlüsselung der Nachricht. with-password.1=Ermöglicht Entschlüsselung basierend auf im "CIPHERTEXT" enthaltenen "SKESK" Paketen. with-password.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). 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 50ab7c9..f21b79a 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-sign.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-sign.properties @@ -3,12 +3,12 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Create a detached signature on the data from standard input no-armor=ASCII armor the output -as.0=Specify the output format of the signed message +as.0=Specify the output format of the signed message. as.1=Defaults to 'binary'. as.2=If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53. 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...). -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) +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 # Generic TODO: Remove when bumping picocli to 4.7.0 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 9b843cc..ecc75b3 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-verify.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-verify.properties @@ -5,10 +5,10 @@ usage.header=Verify a detached signature over the data from standard input not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) not-before.1=Reject signatures with a creation date not in range. not-before.2=Defaults to beginning of time ("-"). -not-after.1=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) -not-after.2=Reject signatures with a creation date not in range. -not-after.3=Defaults to current system time ("now").\ -not-after.4 = Accepts special value "-" for end of time. +not-after.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) +not-after.1=Reject signatures with a creation date not in range. +not-after.2=Defaults to current system time ("now"). +not-after.3=Accepts special value "-" for end of time. SIGNATURE[0]=Detached signature CERT[0..*]=Public key certificates for signature verification diff --git a/sop-java-picocli/src/main/resources/msg_encrypt.properties b/sop-java-picocli/src/main/resources/msg_encrypt.properties index c04ce32..7c3b218 100644 --- a/sop-java-picocli/src/main/resources/msg_encrypt.properties +++ b/sop-java-picocli/src/main/resources/msg_encrypt.properties @@ -5,7 +5,7 @@ usage.header=Encrypt a message from standard input no-armor=ASCII armor the output as=Type of the input data. Defaults to 'binary' with-password.0=Encrypt the message with a password. -with-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) +with-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). sign-with=Sign the output with a private key profile=Profile identifier to switch between profiles with-key-password.0=Passphrase to unlock the secret key(s). 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 88584e9..888cbfb 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-sign.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-sign.properties @@ -3,14 +3,14 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Create an inline-signed message from data on standard input no-armor=ASCII armor the output -as.0=Specify the signature format of the signed message +as.0=Specify the signature format of the signed message. as.1='text' and 'binary' will produce inline-signed messages. -as.2='cleartextsigned' will make use of the cleartext signature framework. +as.2='clearsigned' will make use of the cleartext signature framework. as.3=Defaults to 'binary'. as.4=If '--as=text' and the input data is not valid UTF-8, inline-sign fails with return code 53. 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...). -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) +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 # Generic TODO: Remove when bumping picocli to 4.7.0 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 10be2f3..d0606d7 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 @@ -5,7 +5,7 @@ usage.header=Signiere eine Nachricht von Standard-Eingabe mit eingebetteten Sign no-armor=Schütze Ausgabe mit ASCII Armor as.0=Bestimme Signaturformat der Nachricht. as.1='text' und 'binary' resultieren in eingebettete Signaturen. -as.2='cleartextsigned' wird die Nachricht Klartext-signieren. +as.2='clearsigned' wird die Nachricht Klartext-signieren. as.3=Standardmäßig: 'binary'. as.4=Ist die Standard-Eingabe nicht UTF-8 kodiert und '--as=text' gesetzt, so wird inline-sign Fehlercode 53 zurückgeben. with-key-password.0=Passwort zum Entsperren des privaten Schlüssels diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index b41d314..52c5368 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -47,7 +47,7 @@ sop.error.input.malformed_not_after=Invalid date string supplied as value of '-- sop.error.input.malformed_not_before=Invalid date string supplied as value of '--not-before'. sop.error.input.stdin_not_a_message=Standard Input appears not to contain a valid OpenPGP message. sop.error.input.stdin_not_a_private_key=Standard Input appears not to contain a valid OpenPGP secret key. -sop.error.input.stdin_not_openpgp_data=Standard Input appears not to contain valid OpenPGP data +sop.error.input.stdin_not_openpgp_data=Standard Input appears not to contain valid OpenPGP data. ## Indirect Data Types sop.error.indirect_data_type.ambiguous_filename=File name '%s' is ambiguous. File with the same name exists on the filesystem. sop.error.indirect_data_type.environment_variable_not_set=Environment variable '%s' not set. 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 fe460e9..573a82f 100644 --- a/sop-java-picocli/src/main/resources/msg_version_de.properties +++ b/sop-java-picocli/src/main/resources/msg_version_de.properties @@ -4,7 +4,7 @@ usage.header=Zeige Versionsinformationen über das Programm 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. +sop-spec=Gebe die neuste Revision der SOP Spezifikation aus, welche von dieser Implementierung umgesetzt wird # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 From 19d6b7e142ec15eed42ab837ed77b5f837fb57d0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 12:44:01 +0200 Subject: [PATCH 026/298] Verification: Make use of Optional for signature mode and description --- sop-java/src/main/java/sop/Verification.java | 42 +++++++++++++------ .../assertions/VerificationAssert.java | 8 ++-- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/sop-java/src/main/java/sop/Verification.java b/sop-java/src/main/java/sop/Verification.java index f18826e..12ed819 100644 --- a/sop-java/src/main/java/sop/Verification.java +++ b/sop-java/src/main/java/sop/Verification.java @@ -4,11 +4,12 @@ package sop; -import java.util.Date; - import sop.enums.SignatureMode; +import sop.util.Optional; import sop.util.UTCUtil; +import java.util.Date; + /** * Class bundling information about a verified signature. */ @@ -17,8 +18,8 @@ public class Verification { private final Date creationTime; private final String signingKeyFingerprint; private final String signingCertFingerprint; - private final SignatureMode signatureMode; - private final String description; + private final Optional signatureMode; + private final Optional description; private static final String MODE = "mode:"; @@ -30,7 +31,7 @@ public class Verification { * @param signingCertFingerprint fingerprint of the certificate */ public Verification(Date creationTime, String signingKeyFingerprint, String signingCertFingerprint) { - this(creationTime, signingKeyFingerprint, signingCertFingerprint, null, null); + this(creationTime, signingKeyFingerprint, signingCertFingerprint, Optional.ofEmpty(), Optional.ofEmpty()); } /** @@ -43,11 +44,28 @@ public class Verification { * @param description free-form description, e.g.

certificate from dkg.asc
(optional, may be
null
) */ public Verification(Date creationTime, String signingKeyFingerprint, String signingCertFingerprint, SignatureMode signatureMode, String description) { + this( + creationTime, + signingKeyFingerprint, + signingCertFingerprint, + Optional.ofNullable(signatureMode), + Optional.ofNullable(nullSafeTrim(description)) + ); + } + + private Verification(Date creationTime, String signingKeyFingerprint, String signingCertFingerprint, Optional signatureMode, Optional description) { this.creationTime = creationTime; this.signingKeyFingerprint = signingKeyFingerprint; this.signingCertFingerprint = signingCertFingerprint; this.signatureMode = signatureMode; - this.description = description == null ? null : description.trim(); + this.description = description; + } + + private static String nullSafeTrim(String string) { + if (string == null) { + return null; + } + return string.trim(); } public static Verification fromString(String toString) { @@ -121,7 +139,7 @@ public class Verification { * * @return signature mode */ - public SignatureMode getSignatureMode() { + public Optional getSignatureMode() { return signatureMode; } @@ -131,7 +149,7 @@ public class Verification { * * @return description */ - public String getDescription() { + public Optional getDescription() { return description; } @@ -144,12 +162,12 @@ public class Verification { .append(' ') .append(getSigningCertFingerprint()); - if (signatureMode != null) { - sb.append(' ').append(MODE).append(signatureMode); + if (signatureMode.isPresent()) { + sb.append(' ').append(MODE).append(signatureMode.get()); } - if (description != null) { - sb.append(' ').append(description); + if (description.isPresent()) { + sb.append(' ').append(description.get()); } return sb.toString(); diff --git a/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationAssert.java b/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationAssert.java index 35e3ed6..63fd237 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationAssert.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationAssert.java @@ -45,12 +45,12 @@ public final class VerificationAssert { } public VerificationAssert hasDescription(String description) { - assertEquals(description, verification.getDescription()); + assertEquals(description, verification.getDescription().get()); return this; } public VerificationAssert hasDescriptionOrNull(String description) { - if (verification.getDescription() == null) { + if (verification.getDescription().isEmpty()) { return this; } @@ -58,12 +58,12 @@ public final class VerificationAssert { } public VerificationAssert hasMode(SignatureMode mode) { - assertEquals(mode, verification.getSignatureMode()); + assertEquals(mode, verification.getSignatureMode().get()); return this; } public VerificationAssert hasModeOrNull(SignatureMode mode) { - if (verification.getSignatureMode() == null) { + if (verification.getSignatureMode().isEmpty()) { return this; } return hasMode(mode); From 44e6dd2180235bcefdbcc2a0558ce3888b726ce8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 12:44:40 +0200 Subject: [PATCH 027/298] Depend on findbugs:jsr305 for @Nullable etc. annotations --- sop-java/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sop-java/build.gradle b/sop-java/build.gradle index c958610..ca546bf 100644 --- a/sop-java/build.gradle +++ b/sop-java/build.gradle @@ -19,6 +19,10 @@ dependencies { testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" testFixturesImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testFixturesImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" + + // @Nullable, @Nonnull annotations + implementation "com.google.code.findbugs:jsr305:3.0.2" + } test { From 8b8863c6dfc7dc9bb2fe87cc182cf8df2a03d4fa Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 12:52:12 +0200 Subject: [PATCH 028/298] Verification: Annotate with @Nonnull, @Nullable --- sop-java/src/main/java/sop/Verification.java | 28 ++++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/sop-java/src/main/java/sop/Verification.java b/sop-java/src/main/java/sop/Verification.java index 12ed819..d48a0b7 100644 --- a/sop-java/src/main/java/sop/Verification.java +++ b/sop-java/src/main/java/sop/Verification.java @@ -8,6 +8,8 @@ import sop.enums.SignatureMode; import sop.util.Optional; import sop.util.UTCUtil; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Date; /** @@ -30,7 +32,9 @@ public class Verification { * @param signingKeyFingerprint fingerprint of the signing (sub-) key * @param signingCertFingerprint fingerprint of the certificate */ - public Verification(Date creationTime, String signingKeyFingerprint, String signingCertFingerprint) { + public Verification(@Nonnull Date creationTime, + @Nonnull String signingKeyFingerprint, + @Nonnull String signingCertFingerprint) { this(creationTime, signingKeyFingerprint, signingCertFingerprint, Optional.ofEmpty(), Optional.ofEmpty()); } @@ -43,7 +47,11 @@ public class Verification { * @param signatureMode signature mode (optional, may be
null
) * @param description free-form description, e.g.
certificate from dkg.asc
(optional, may be
null
) */ - public Verification(Date creationTime, String signingKeyFingerprint, String signingCertFingerprint, SignatureMode signatureMode, String description) { + public Verification(@Nonnull Date creationTime, + @Nonnull String signingKeyFingerprint, + @Nonnull String signingCertFingerprint, + @Nullable SignatureMode signatureMode, + @Nullable String description) { this( creationTime, signingKeyFingerprint, @@ -53,7 +61,11 @@ public class Verification { ); } - private Verification(Date creationTime, String signingKeyFingerprint, String signingCertFingerprint, Optional signatureMode, Optional description) { + private Verification(@Nonnull Date creationTime, + @Nonnull String signingKeyFingerprint, + @Nonnull String signingCertFingerprint, + @Nonnull Optional signatureMode, + @Nonnull Optional description) { this.creationTime = creationTime; this.signingKeyFingerprint = signingKeyFingerprint; this.signingCertFingerprint = signingCertFingerprint; @@ -61,14 +73,15 @@ public class Verification { this.description = description; } - private static String nullSafeTrim(String string) { + private static String nullSafeTrim(@Nullable String string) { if (string == null) { return null; } return string.trim(); } - public static Verification fromString(String toString) { + @Nonnull + public static Verification fromString(@Nonnull String toString) { String[] split = toString.trim().split(" "); if (split.length < 3) { throw new IllegalArgumentException("Verification must be of the format 'UTC-DATE OpenPGPFingerprint OpenPGPFingerprint [mode] [info]'"); @@ -111,6 +124,7 @@ public class Verification { * * @return signature creation time */ + @Nonnull public Date getCreationTime() { return creationTime; } @@ -120,6 +134,7 @@ public class Verification { * * @return signing key fingerprint */ + @Nonnull public String getSigningKeyFingerprint() { return signingKeyFingerprint; } @@ -129,6 +144,7 @@ public class Verification { * * @return signing certificate fingerprint */ + @Nonnull public String getSigningCertFingerprint() { return signingCertFingerprint; } @@ -139,6 +155,7 @@ public class Verification { * * @return signature mode */ + @Nonnull public Optional getSignatureMode() { return signatureMode; } @@ -149,6 +166,7 @@ public class Verification { * * @return description */ + @Nonnull public Optional getDescription() { return description; } From 0308732328d6a3879c3b9d6a66ffee2c5d1782e8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 13:07:08 +0200 Subject: [PATCH 029/298] Make UTCUtil.parseUTCDate() throw instead of returning null for malformed inputs --- .../cli/picocli/commands/AbstractSopCmd.java | 30 ++++++++++++++----- .../cli/picocli/commands/DecryptCmdTest.java | 5 ++-- .../cli/picocli/commands/VerifyCmdTest.java | 9 +++--- sop-java/src/main/java/sop/Verification.java | 13 ++++++-- sop-java/src/main/java/sop/util/UTCUtil.java | 12 ++++++-- .../java/sop/util/ByteArrayAndResultTest.java | 3 +- .../java/sop/util/ReadyWithResultTest.java | 3 +- .../src/test/java/sop/util/UTCUtilTest.java | 18 +++++------ .../test/java/sop/util/VerificationTest.java | 11 +++---- .../java/sop/testsuite/TestData.java | 13 ++++++-- .../operation/EncryptDecryptTest.java | 5 ++-- 11 files changed, 85 insertions(+), 37 deletions(-) diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java index d155e9c..9aec5a7 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java @@ -18,6 +18,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.text.ParseException; import java.util.Collection; import java.util.Date; import java.util.Locale; @@ -246,21 +247,36 @@ public abstract class AbstractSopCmd implements Runnable { } public Date parseNotAfter(String notAfter) { - Date date = notAfter.equals("now") ? new Date() : notAfter.equals("-") ? END_OF_TIME : UTCUtil.parseUTCDate(notAfter); - if (date == null) { + if (notAfter.equals("now")) { + return new Date(); + } + + if (notAfter.equals("-")) { + return END_OF_TIME; + } + + try { + return UTCUtil.parseUTCDate(notAfter); + } catch (ParseException e) { String errorMsg = getMsg("sop.error.input.malformed_not_after"); throw new IllegalArgumentException(errorMsg); } - return date; } public Date parseNotBefore(String notBefore) { - Date date = notBefore.equals("now") ? new Date() : notBefore.equals("-") ? BEGINNING_OF_TIME : UTCUtil.parseUTCDate(notBefore); - if (date == null) { + if (notBefore.equals("now")) { + return new Date(); + } + + if (notBefore.equals("-")) { + return BEGINNING_OF_TIME; + } + + try { + return UTCUtil.parseUTCDate(notBefore); + } catch (ParseException e) { String errorMsg = getMsg("sop.error.input.malformed_not_before"); throw new IllegalArgumentException(errorMsg); } - return date; } - } 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 b445a6d..e3dd198 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 @@ -31,6 +31,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.text.ParseException; import java.util.Collections; import java.util.Date; @@ -187,7 +188,7 @@ public class DecryptCmdTest { } @Test - public void assertSessionKeyAndVerificationsIsProperlyWrittenToSessionKeyFile() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { + public void assertSessionKeyAndVerificationsIsProperlyWrittenToSessionKeyFile() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException, ParseException { Date signDate = UTCUtil.parseUTCDate("2022-11-07T15:01:24Z"); String keyFP = "F9E6F53F7201C60A87064EAB0B27F2B0760A1209"; String certFP = "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B"; @@ -281,7 +282,7 @@ public class DecryptCmdTest { } @Test - public void verifyOutIsProperlyWritten() throws IOException, SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData { + public void verifyOutIsProperlyWritten() throws IOException, SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, ParseException { File certFile = File.createTempFile("verify-out-cert", ".asc"); File verifyOut = new File(certFile.getParent(), "verify-out.txt"); if (verifyOut.exists()) { 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 c33cf74..50a8043 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 @@ -16,6 +16,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; +import java.text.ParseException; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -41,7 +42,7 @@ public class VerifyCmdTest { PrintStream originalSout; @BeforeEach - public void prepare() throws SOPGPException.UnsupportedOption, SOPGPException.BadData, SOPGPException.NoSignature, IOException { + public void prepare() throws SOPGPException.UnsupportedOption, SOPGPException.BadData, SOPGPException.NoSignature, IOException, ParseException { originalSout = System.out; detachedVerify = mock(DetachedVerify.class); @@ -73,7 +74,7 @@ public class VerifyCmdTest { } @Test - public void notAfter_passedDown() throws SOPGPException.UnsupportedOption { + 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()}); verify(detachedVerify, times(1)).notAfter(date); @@ -100,7 +101,7 @@ public class VerifyCmdTest { } @Test - public void notBefore_passedDown() throws SOPGPException.UnsupportedOption { + 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()}); verify(detachedVerify, times(1)).notBefore(date); @@ -178,7 +179,7 @@ public class VerifyCmdTest { } @Test - public void resultIsPrintedProperly() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { + public void resultIsPrintedProperly() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData, ParseException { when(detachedVerify.data((InputStream) any())).thenReturn(Arrays.asList( new Verification(UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"), "EB85BB5FA33A75E15E944E63F231550C4F47E38E", diff --git a/sop-java/src/main/java/sop/Verification.java b/sop-java/src/main/java/sop/Verification.java index d48a0b7..140e23b 100644 --- a/sop-java/src/main/java/sop/Verification.java +++ b/sop-java/src/main/java/sop/Verification.java @@ -10,6 +10,7 @@ import sop.util.UTCUtil; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.text.ParseException; import java.util.Date; /** @@ -89,7 +90,7 @@ public class Verification { if (split.length == 3) { return new Verification( - UTCUtil.parseUTCDate(split[0]), // timestamp + parseUTCDate(split[0]), // timestamp split[1], // key FP split[2] // cert FP ); @@ -111,7 +112,7 @@ public class Verification { } return new Verification( - UTCUtil.parseUTCDate(split[0]), // timestamp + parseUTCDate(split[0]), // timestamp split[1], // key FP split[2], // cert FP mode, // signature mode @@ -119,6 +120,14 @@ public class Verification { ); } + private static Date parseUTCDate(String utcFormatted) { + try { + return UTCUtil.parseUTCDate(utcFormatted); + } catch (ParseException e) { + throw new IllegalArgumentException("Malformed UTC timestamp.", e); + } + } + /** * Return the signatures' creation time. * diff --git a/sop-java/src/main/java/sop/util/UTCUtil.java b/sop-java/src/main/java/sop/util/UTCUtil.java index 8ef7e77..3849258 100644 --- a/sop-java/src/main/java/sop/util/UTCUtil.java +++ b/sop-java/src/main/java/sop/util/UTCUtil.java @@ -4,6 +4,7 @@ package sop.util; +import javax.annotation.Nonnull; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; @@ -33,15 +34,22 @@ public class UTCUtil { * @param dateString string * @return date */ - public static Date parseUTCDate(String dateString) { + @Nonnull + public static Date parseUTCDate(String dateString) throws ParseException { + ParseException exception = null; for (SimpleDateFormat parser : UTC_PARSERS) { try { return parser.parse(dateString); } catch (ParseException e) { + // Store first exception (that of UTC_FORMATTER) to throw if we fail to parse the date + if (exception == null) { + exception = e; + } // Try next parser } } - return null; + // No parser worked, so we throw the store exception + throw exception; } /** diff --git a/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java b/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java index 8ae1859..a1413b4 100644 --- a/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java +++ b/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import java.nio.charset.StandardCharsets; +import java.text.ParseException; import java.util.Collections; import java.util.List; @@ -18,7 +19,7 @@ import sop.Verification; public class ByteArrayAndResultTest { @Test - public void testCreationAndGetters() { + public void testCreationAndGetters() throws ParseException { byte[] bytes = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); List result = Collections.singletonList( new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"), diff --git a/sop-java/src/test/java/sop/util/ReadyWithResultTest.java b/sop-java/src/test/java/sop/util/ReadyWithResultTest.java index 97841fa..1207c5c 100644 --- a/sop-java/src/test/java/sop/util/ReadyWithResultTest.java +++ b/sop-java/src/test/java/sop/util/ReadyWithResultTest.java @@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.text.ParseException; import java.util.Collections; import java.util.List; @@ -22,7 +23,7 @@ import sop.exception.SOPGPException; public class ReadyWithResultTest { @Test - public void testReadyWithResult() throws SOPGPException.NoSignature, IOException { + public void testReadyWithResult() throws SOPGPException.NoSignature, IOException, ParseException { byte[] data = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); List result = Collections.singletonList( new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"), diff --git a/sop-java/src/test/java/sop/util/UTCUtilTest.java b/sop-java/src/test/java/sop/util/UTCUtilTest.java index 18de817..f6ccbc8 100644 --- a/sop-java/src/test/java/sop/util/UTCUtilTest.java +++ b/sop-java/src/test/java/sop/util/UTCUtilTest.java @@ -4,12 +4,13 @@ package sop.util; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import org.junit.jupiter.api.Test; +import java.text.ParseException; import java.util.Date; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; /** * Test parsing some date examples from the stateless OpenPGP CLI spec. @@ -19,30 +20,29 @@ import org.junit.jupiter.api.Test; public class UTCUtilTest { @Test - public void parseExample1() { + public void parseExample1() throws ParseException { String timestamp = "2019-10-29T12:11:04+00:00"; Date date = UTCUtil.parseUTCDate(timestamp); assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date)); } @Test - public void parseExample2() { + public void parseExample2() throws ParseException { String timestamp = "2019-10-24T23:48:29Z"; Date date = UTCUtil.parseUTCDate(timestamp); assertEquals("2019-10-24T23:48:29Z", UTCUtil.formatUTCDate(date)); } @Test - public void parseExample3() { + public void parseExample3() throws ParseException { String timestamp = "20191029T121104Z"; Date date = UTCUtil.parseUTCDate(timestamp); assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date)); } @Test - public void invalidDateReturnsNull() { + public void invalidDateThrows() { String invalidTimestamp = "foobar"; - Date expectNull = UTCUtil.parseUTCDate(invalidTimestamp); - assertNull(expectNull); + assertThrows(ParseException.class, () -> UTCUtil.parseUTCDate(invalidTimestamp)); } } diff --git a/sop-java/src/test/java/sop/util/VerificationTest.java b/sop-java/src/test/java/sop/util/VerificationTest.java index b292688..c4f864e 100644 --- a/sop-java/src/test/java/sop/util/VerificationTest.java +++ b/sop-java/src/test/java/sop/util/VerificationTest.java @@ -9,6 +9,7 @@ import sop.Verification; import sop.enums.SignatureMode; import sop.testsuite.assertions.VerificationAssert; +import java.text.ParseException; import java.util.Date; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -16,7 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class VerificationTest { @Test - public void limitedConstructorTest() { + public void limitedConstructorTest() throws ParseException { Date signDate = UTCUtil.parseUTCDate("2022-11-07T15:01:24Z"); String keyFP = "F9E6F53F7201C60A87064EAB0B27F2B0760A1209"; String certFP = "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B"; @@ -32,7 +33,7 @@ public class VerificationTest { } @Test - public void limitedParsingTest() { + public void limitedParsingTest() throws ParseException { String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B"; Verification verification = Verification.fromString(string); assertEquals(string, verification.toString()); @@ -44,7 +45,7 @@ public class VerificationTest { } @Test - public void parsingWithModeTest() { + public void parsingWithModeTest() throws ParseException { String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B mode:text"; Verification verification = Verification.fromString(string); assertEquals(string, verification.toString()); @@ -56,7 +57,7 @@ public class VerificationTest { } @Test - public void extendedConstructorTest() { + public void extendedConstructorTest() throws ParseException { Date signDate = UTCUtil.parseUTCDate("2022-11-07T15:01:24Z"); String keyFP = "F9E6F53F7201C60A87064EAB0B27F2B0760A1209"; String certFP = "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B"; @@ -73,7 +74,7 @@ public class VerificationTest { } @Test - public void extendedParsingTest() { + public void extendedParsingTest() throws ParseException { String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B mode:binary certificate from dkg.asc"; Verification verification = Verification.fromString(string); assertEquals(string, verification.toString()); diff --git a/sop-java/src/testFixtures/java/sop/testsuite/TestData.java b/sop-java/src/testFixtures/java/sop/testsuite/TestData.java index e1d35c2..386c411 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/TestData.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/TestData.java @@ -7,6 +7,7 @@ package sop.testsuite; import sop.util.UTCUtil; import java.nio.charset.StandardCharsets; +import java.text.ParseException; import java.util.Date; public class TestData { @@ -59,7 +60,7 @@ public class TestData { "sfcfswMA\n" + "=RDAo\n" + "-----END PGP MESSAGE-----"; - public static final Date ALICE_INLINE_SIGNED_MESSAGE_DATE = UTCUtil.parseUTCDate("2023-01-13T17:20:47Z"); + public static final Date ALICE_INLINE_SIGNED_MESSAGE_DATE = parseUTCDate("2023-01-13T17:20:47Z"); // signature over PLAINTEXT public static final String ALICE_DETACHED_SIGNED_MESSAGE = "-----BEGIN PGP SIGNATURE-----\n" + "\n" + @@ -68,7 +69,7 @@ public class TestData { "0K1UgT5roym9Fln8U5W8R03TSbfNiwE=\n" + "=bxPN\n" + "-----END PGP SIGNATURE-----"; - public static final Date ALICE_DETACHED_SIGNED_MESSAGE_DATE = UTCUtil.parseUTCDate("2023-01-13T16:57:57Z"); + public static final Date ALICE_DETACHED_SIGNED_MESSAGE_DATE = parseUTCDate("2023-01-13T16:57:57Z"); // 'Bob' key from draft-bre-openpgp-samples-00 public static final String BOB_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + @@ -421,4 +422,12 @@ public class TestData { public static final byte[] BEGIN_PGP_SIGNATURE = "-----BEGIN PGP SIGNATURE-----\n".getBytes(StandardCharsets.UTF_8); public static final byte[] END_PGP_SIGNATURE = "-----END PGP SIGNATURE-----".getBytes(StandardCharsets.UTF_8); public static final byte[] BEGIN_PGP_SIGNED_MESSAGE = "-----BEGIN PGP SIGNED MESSAGE-----\n".getBytes(StandardCharsets.UTF_8); + + private static Date parseUTCDate(String utcFormatted) { + try { + return UTCUtil.parseUTCDate(utcFormatted); + } catch (ParseException e) { + throw new IllegalArgumentException("Malformed UTC timestamp.", e); + } + } } 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 23f117e..0c382dc 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java @@ -21,6 +21,7 @@ import sop.util.UTCUtil; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.text.ParseException; import java.util.Date; import java.util.List; import java.util.stream.Stream; @@ -231,7 +232,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") - public void decryptVerifyNotAfterTest(SOP sop) { + public void decryptVerifyNotAfterTest(SOP sop) throws ParseException { byte[] message = ("-----BEGIN PGP MESSAGE-----\n" + "\n" + "wV4DR2b2udXyHrYSAQdAwlOwwyxFDJta5+H9abgSj8jum9v7etUc9usdrElESmow\n" + @@ -265,7 +266,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") - public void decryptVerifyNotBeforeTest(SOP sop) { + public void decryptVerifyNotBeforeTest(SOP sop) throws ParseException { byte[] message = ("-----BEGIN PGP MESSAGE-----\n" + "\n" + "wV4DR2b2udXyHrYSAQdAwlOwwyxFDJta5+H9abgSj8jum9v7etUc9usdrElESmow\n" + From 8eba099146d6d1a390c5560118ecb1d5d902c92c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 13:15:44 +0200 Subject: [PATCH 030/298] UTF8Util.decodeUTF8(): throw CharacterCodingException instead of PasswordNotHumanReadable --- .../src/main/java/sop/operation/GenerateKey.java | 7 ++++++- sop-java/src/main/java/sop/util/UTF8Util.java | 16 ++++++---------- .../src/test/java/sop/util/UTF8UtilTest.java | 10 +++++----- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/sop-java/src/main/java/sop/operation/GenerateKey.java b/sop-java/src/main/java/sop/operation/GenerateKey.java index 27a3b19..b788785 100644 --- a/sop-java/src/main/java/sop/operation/GenerateKey.java +++ b/sop-java/src/main/java/sop/operation/GenerateKey.java @@ -6,6 +6,7 @@ package sop.operation; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.CharacterCodingException; import sop.Profile; import sop.Ready; @@ -54,7 +55,11 @@ public interface GenerateKey { default GenerateKey withKeyPassword(byte[] password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - return withKeyPassword(UTF8Util.decodeUTF8(password)); + try { + return withKeyPassword(UTF8Util.decodeUTF8(password)); + } catch (CharacterCodingException e) { + throw new SOPGPException.PasswordNotHumanReadable(); + } } /** diff --git a/sop-java/src/main/java/sop/util/UTF8Util.java b/sop-java/src/main/java/sop/util/UTF8Util.java index ec3c22d..a59ff43 100644 --- a/sop-java/src/main/java/sop/util/UTF8Util.java +++ b/sop-java/src/main/java/sop/util/UTF8Util.java @@ -4,8 +4,6 @@ package sop.util; -import sop.exception.SOPGPException; - import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; @@ -15,7 +13,8 @@ import java.nio.charset.CodingErrorAction; public class UTF8Util { - private static final CharsetDecoder UTF8Decoder = Charset.forName("UTF8") + public static final Charset UTF8 = Charset.forName("UTF8"); + private static final CharsetDecoder UTF8Decoder = UTF8 .newDecoder() .onUnmappableCharacter(CodingErrorAction.REPORT) .onMalformedInput(CodingErrorAction.REPORT); @@ -28,13 +27,10 @@ public class UTF8Util { * * @return decoded string */ - public static String decodeUTF8(byte[] data) { + public static String decodeUTF8(byte[] data) + throws CharacterCodingException { ByteBuffer byteBuffer = ByteBuffer.wrap(data); - try { - CharBuffer charBuffer = UTF8Decoder.decode(byteBuffer); - return charBuffer.toString(); - } catch (CharacterCodingException e) { - throw new SOPGPException.PasswordNotHumanReadable(); - } + CharBuffer charBuffer = UTF8Decoder.decode(byteBuffer); + return charBuffer.toString(); } } diff --git a/sop-java/src/test/java/sop/util/UTF8UtilTest.java b/sop-java/src/test/java/sop/util/UTF8UtilTest.java index 775d273..95906bc 100644 --- a/sop-java/src/test/java/sop/util/UTF8UtilTest.java +++ b/sop-java/src/test/java/sop/util/UTF8UtilTest.java @@ -5,8 +5,8 @@ package sop.util; import org.junit.jupiter.api.Test; -import sop.exception.SOPGPException; +import java.nio.charset.CharacterCodingException; import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -15,7 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; public class UTF8UtilTest { @Test - public void testValidUtf8Decoding() { + public void testValidUtf8Decoding() throws CharacterCodingException { String utf8String = "Hello, World\n"; String decoded = UTF8Util.decodeUTF8(utf8String.getBytes(StandardCharsets.UTF_8)); @@ -29,11 +29,11 @@ public class UTF8UtilTest { */ @Test public void testInvalidUtf8StringThrows() { - assertThrows(SOPGPException.PasswordNotHumanReadable.class, + assertThrows(CharacterCodingException.class, () -> UTF8Util.decodeUTF8(new byte[] {(byte) 0xa0, (byte) 0xa1})); - assertThrows(SOPGPException.PasswordNotHumanReadable.class, + assertThrows(CharacterCodingException.class, () -> UTF8Util.decodeUTF8(new byte[] {(byte) 0xc0, (byte) 0xaf})); - assertThrows(SOPGPException.PasswordNotHumanReadable.class, + assertThrows(CharacterCodingException.class, () -> UTF8Util.decodeUTF8(new byte[] {(byte) 0x80, (byte) 0xbf})); } } From 8aded17f1091d8ca1e97e0065df06572c1d62bb1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 13:17:58 +0200 Subject: [PATCH 031/298] Use UTF8Util.UTF8 constant --- sop-java/src/main/java/sop/Profile.java | 4 ++-- .../src/main/java/sop/operation/AbstractSign.java | 4 ++-- sop-java/src/main/java/sop/operation/Decrypt.java | 4 ++-- sop-java/src/main/java/sop/operation/Encrypt.java | 12 ++++++------ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/sop-java/src/main/java/sop/Profile.java b/sop-java/src/main/java/sop/Profile.java index 801f748..4a981f4 100644 --- a/sop-java/src/main/java/sop/Profile.java +++ b/sop-java/src/main/java/sop/Profile.java @@ -4,7 +4,7 @@ package sop; -import java.nio.charset.Charset; +import sop.util.UTF8Util; /** * Tuple class bundling a profile name and description. @@ -77,6 +77,6 @@ public class Profile { */ private static boolean exceeds1000CharLineLimit(Profile profile) { String line = profile.toString(); - return line.getBytes(Charset.forName("UTF8")).length > 1000; + return line.getBytes(UTF8Util.UTF8).length > 1000; } } diff --git a/sop-java/src/main/java/sop/operation/AbstractSign.java b/sop-java/src/main/java/sop/operation/AbstractSign.java index 3f9d6fc..508d741 100644 --- a/sop-java/src/main/java/sop/operation/AbstractSign.java +++ b/sop-java/src/main/java/sop/operation/AbstractSign.java @@ -5,11 +5,11 @@ package sop.operation; import sop.exception.SOPGPException; +import sop.util.UTF8Util; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.Charset; public interface AbstractSign { @@ -67,7 +67,7 @@ public interface AbstractSign { default T withKeyPassword(String password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { - return withKeyPassword(password.getBytes(Charset.forName("UTF8"))); + return withKeyPassword(password.getBytes(UTF8Util.UTF8)); } /** diff --git a/sop-java/src/main/java/sop/operation/Decrypt.java b/sop-java/src/main/java/sop/operation/Decrypt.java index d695032..0123bbc 100644 --- a/sop-java/src/main/java/sop/operation/Decrypt.java +++ b/sop-java/src/main/java/sop/operation/Decrypt.java @@ -8,11 +8,11 @@ import sop.DecryptionResult; import sop.ReadyWithResult; import sop.SessionKey; import sop.exception.SOPGPException; +import sop.util.UTF8Util; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.Charset; import java.util.Date; public interface Decrypt { @@ -138,7 +138,7 @@ public interface Decrypt { default Decrypt withKeyPassword(String password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { - return withKeyPassword(password.getBytes(Charset.forName("UTF8"))); + return withKeyPassword(password.getBytes(UTF8Util.UTF8)); } /** diff --git a/sop-java/src/main/java/sop/operation/Encrypt.java b/sop-java/src/main/java/sop/operation/Encrypt.java index fed1210..b380d32 100644 --- a/sop-java/src/main/java/sop/operation/Encrypt.java +++ b/sop-java/src/main/java/sop/operation/Encrypt.java @@ -4,15 +4,15 @@ package sop.operation; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; - import sop.Profile; import sop.Ready; import sop.enums.EncryptAs; import sop.exception.SOPGPException; +import sop.util.UTF8Util; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; public interface Encrypt { @@ -82,7 +82,7 @@ public interface Encrypt { default Encrypt withKeyPassword(String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - return withKeyPassword(password.getBytes(Charset.forName("UTF8"))); + return withKeyPassword(password.getBytes(UTF8Util.UTF8)); } /** From 49fd7143cf71d0565daa73b1f31ee79195e758d2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 13:26:01 +0200 Subject: [PATCH 032/298] Update CHANGELOG --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a4e918..b9bc020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,14 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 6.0.1-SNAPSHOT +## 6.1.0-SNAPSHOT - `listProfiles()`: Add shortcut methods `generateKey()` and `encrypt()` - Add DSL for testing `Verification` results -- `external-sop`: Properly map error codes to new exception types: +- `Verification`: Return `Optional<>` for `getSignatureMode()` and `getDescription()` +- `sop-java`: Add dependency on `com.google.code.findbugs:jsr305` for `@Nullable`, `@Nonnull` annotations +- `UTCUtil`: `parseUTCDate()` is now `@Nonnull` and throws a `ParseException` for invalid inputs +- `UTF8Util`: `decodeUTF8()` now throws `CharacterCodingException` instead of `SOPGPException.PasswordNotHumanReadable` +- `external-sop`: Properly map error codes to new exception types (ported from `5.0.1`): - `UNSUPPORTED_PROFILE` - `INCOMPATIBLE_OPTIONS` From 7ea46a1916eefbd4b50c8be496dcb4300794b580 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 14:23:58 +0200 Subject: [PATCH 033/298] Move tests --- .../src/test/java/sop/{util => }/ByteArrayAndResultTest.java | 3 ++- sop-java/src/test/java/sop/{util => }/MicAlgTest.java | 2 +- sop-java/src/test/java/sop/{util => }/ReadyTest.java | 2 +- sop-java/src/test/java/sop/{util => }/ReadyWithResultTest.java | 3 ++- sop-java/src/test/java/sop/{util => }/SessionKeyTest.java | 3 ++- sop-java/src/test/java/sop/{util => }/SigningResultTest.java | 2 +- 6 files changed, 9 insertions(+), 6 deletions(-) rename sop-java/src/test/java/sop/{util => }/ByteArrayAndResultTest.java (96%) rename sop-java/src/test/java/sop/{util => }/MicAlgTest.java (98%) rename sop-java/src/test/java/sop/{util => }/ReadyTest.java (97%) rename sop-java/src/test/java/sop/{util => }/ReadyWithResultTest.java (97%) rename sop-java/src/test/java/sop/{util => }/SessionKeyTest.java (98%) rename sop-java/src/test/java/sop/{util => }/SigningResultTest.java (96%) diff --git a/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java b/sop-java/src/test/java/sop/ByteArrayAndResultTest.java similarity index 96% rename from sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java rename to sop-java/src/test/java/sop/ByteArrayAndResultTest.java index a1413b4..8d89842 100644 --- a/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java +++ b/sop-java/src/test/java/sop/ByteArrayAndResultTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package sop.util; +package sop; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -15,6 +15,7 @@ import java.util.List; import org.junit.jupiter.api.Test; import sop.ByteArrayAndResult; import sop.Verification; +import sop.util.UTCUtil; public class ByteArrayAndResultTest { diff --git a/sop-java/src/test/java/sop/util/MicAlgTest.java b/sop-java/src/test/java/sop/MicAlgTest.java similarity index 98% rename from sop-java/src/test/java/sop/util/MicAlgTest.java rename to sop-java/src/test/java/sop/MicAlgTest.java index f720c85..a2cbf73 100644 --- a/sop-java/src/test/java/sop/util/MicAlgTest.java +++ b/sop-java/src/test/java/sop/MicAlgTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package sop.util; +package sop; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/sop-java/src/test/java/sop/util/ReadyTest.java b/sop-java/src/test/java/sop/ReadyTest.java similarity index 97% rename from sop-java/src/test/java/sop/util/ReadyTest.java rename to sop-java/src/test/java/sop/ReadyTest.java index 07fa090..409bfc0 100644 --- a/sop-java/src/test/java/sop/util/ReadyTest.java +++ b/sop-java/src/test/java/sop/ReadyTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package sop.util; +package sop; import static org.junit.jupiter.api.Assertions.assertArrayEquals; diff --git a/sop-java/src/test/java/sop/util/ReadyWithResultTest.java b/sop-java/src/test/java/sop/ReadyWithResultTest.java similarity index 97% rename from sop-java/src/test/java/sop/util/ReadyWithResultTest.java rename to sop-java/src/test/java/sop/ReadyWithResultTest.java index 1207c5c..072314a 100644 --- a/sop-java/src/test/java/sop/util/ReadyWithResultTest.java +++ b/sop-java/src/test/java/sop/ReadyWithResultTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package sop.util; +package sop; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -19,6 +19,7 @@ import sop.ByteArrayAndResult; import sop.ReadyWithResult; import sop.Verification; import sop.exception.SOPGPException; +import sop.util.UTCUtil; public class ReadyWithResultTest { diff --git a/sop-java/src/test/java/sop/util/SessionKeyTest.java b/sop-java/src/test/java/sop/SessionKeyTest.java similarity index 98% rename from sop-java/src/test/java/sop/util/SessionKeyTest.java rename to sop-java/src/test/java/sop/SessionKeyTest.java index db7e89f..7737a29 100644 --- a/sop-java/src/test/java/sop/util/SessionKeyTest.java +++ b/sop-java/src/test/java/sop/SessionKeyTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package sop.util; +package sop; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; import sop.SessionKey; +import sop.util.HexUtil; public class SessionKeyTest { diff --git a/sop-java/src/test/java/sop/util/SigningResultTest.java b/sop-java/src/test/java/sop/SigningResultTest.java similarity index 96% rename from sop-java/src/test/java/sop/util/SigningResultTest.java rename to sop-java/src/test/java/sop/SigningResultTest.java index 0d35cdc..a2f2d77 100644 --- a/sop-java/src/test/java/sop/util/SigningResultTest.java +++ b/sop-java/src/test/java/sop/SigningResultTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package sop.util; +package sop; import static org.junit.jupiter.api.Assertions.assertEquals; From aa88904711becc0ae087e1be47251b3780bb416c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 14:24:59 +0200 Subject: [PATCH 034/298] Add tests for Verification parsing --- .../java/sop/{util => }/VerificationTest.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) rename sop-java/src/test/java/sop/{util => }/VerificationTest.java (83%) diff --git a/sop-java/src/test/java/sop/util/VerificationTest.java b/sop-java/src/test/java/sop/VerificationTest.java similarity index 83% rename from sop-java/src/test/java/sop/util/VerificationTest.java rename to sop-java/src/test/java/sop/VerificationTest.java index c4f864e..c0b0cbc 100644 --- a/sop-java/src/test/java/sop/util/VerificationTest.java +++ b/sop-java/src/test/java/sop/VerificationTest.java @@ -2,17 +2,19 @@ // // SPDX-License-Identifier: Apache-2.0 -package sop.util; +package sop; import org.junit.jupiter.api.Test; import sop.Verification; import sop.enums.SignatureMode; import sop.testsuite.assertions.VerificationAssert; +import sop.util.UTCUtil; import java.text.ParseException; import java.util.Date; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; public class VerificationTest { @@ -89,4 +91,20 @@ public class VerificationTest { .hasMode(null) .hasDescription("certificate from dkg.asc"); } + + @Test + public void missingFingerprintFails() { + String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209"; + assertThrows(IllegalArgumentException.class, () -> Verification.fromString(string)); + } + + @Test + public void malformedTimestampFails() { + String shorter = "'99-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B"; + assertThrows(IllegalArgumentException.class, () -> Verification.fromString(shorter)); + + String longer = "'99-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B mode:binary certificate from dkg.asc"; + assertThrows(IllegalArgumentException.class, () -> Verification.fromString(longer)); + + } } From c479cc8ef3ac2ec4e1da810379fd0ac426a566d7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 14:25:16 +0200 Subject: [PATCH 035/298] Profile: Use Optional for description --- .../operation/ListProfilesExternal.java | 6 +- sop-java/src/main/java/sop/Profile.java | 71 +++++++++++- sop-java/src/test/java/sop/ProfileTest.java | 101 ++++++++++++++++++ 3 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 sop-java/src/test/java/sop/ProfileTest.java diff --git a/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java b/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java index adc0e11..0c76b63 100644 --- a/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java +++ b/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java @@ -38,8 +38,10 @@ public class ListProfilesExternal implements ListProfiles { private static List toProfiles(String output) { List profiles = new ArrayList<>(); for (String line : output.split("\n")) { - String[] split = line.split(": "); - profiles.add(new Profile(split[0], split[1])); + if (line.trim().isEmpty()) { + continue; + } + profiles.add(Profile.parse(line)); } return profiles; } diff --git a/sop-java/src/main/java/sop/Profile.java b/sop-java/src/main/java/sop/Profile.java index 4a981f4..4ea9e71 100644 --- a/sop-java/src/main/java/sop/Profile.java +++ b/sop-java/src/main/java/sop/Profile.java @@ -4,8 +4,12 @@ package sop; +import sop.util.Optional; import sop.util.UTF8Util; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * Tuple class bundling a profile name and description. * @@ -15,7 +19,7 @@ import sop.util.UTF8Util; public class Profile { private final String name; - private final String description; + private final Optional description; /** * Create a new {@link Profile} object. @@ -24,15 +28,62 @@ public class Profile { * @param name profile name * @param description profile description */ - public Profile(String name, String description) { + public Profile(@Nonnull String name, @Nullable String description) { + if (name.trim().isEmpty()) { + throw new IllegalArgumentException("Name cannot be empty."); + } + if (name.contains(":")) { + throw new IllegalArgumentException("Name cannot contain ':'."); + } + if (name.contains(" ") || name.contains("\n") || name.contains("\t") || name.contains("\r")) { + throw new IllegalArgumentException("Name cannot contain whitespace characters."); + } + this.name = name; - this.description = description; + + if (description == null) { + this.description = Optional.ofEmpty(); + } else { + String trimmedDescription = description.trim(); + if (trimmedDescription.isEmpty()) { + this.description = Optional.ofEmpty(); + } else { + this.description = Optional.of(trimmedDescription); + } + } if (exceeds1000CharLineLimit(this)) { throw new IllegalArgumentException("The line representation of a profile MUST NOT exceed 1000 bytes."); } } + public Profile(String name) { + this(name, null); + } + + /** + * Parse a {@link Profile} from its string representation. + * + * @param string string representation + * @return profile + */ + public static Profile parse(String string) { + if (string.contains(": ")) { + // description after colon, e.g. "default: Use implementers recommendations." + String name = string.substring(0, string.indexOf(": ")); + String description = string.substring(string.indexOf(": ") + 2); + return new Profile(name, description.trim()); + } + + if (string.endsWith(":")) { + // empty description, e.g. "default:" + return new Profile(string.substring(0, string.length() - 1)); + } + + // no description + return new Profile(string.trim()); + } + /** * Return the name (also known as identifier) of the profile. * A profile name is a UTF-8 string that has no whitespace in it. @@ -48,6 +99,7 @@ public class Profile { * * @return name */ + @Nonnull public String getName() { return name; } @@ -57,17 +109,26 @@ public class Profile { * * @return description */ - public String getDescription() { + @Nonnull + public Optional getDescription() { return description; } + public boolean hasDescription() { + return description.isPresent(); + } + /** * Convert the profile into a String for displaying. * * @return string */ + @Override public String toString() { - return getName() + ": " + getDescription(); + if (getDescription().isEmpty()) { + return getName(); + } + return getName() + ": " + getDescription().get(); } /** diff --git a/sop-java/src/test/java/sop/ProfileTest.java b/sop-java/src/test/java/sop/ProfileTest.java new file mode 100644 index 0000000..d76d270 --- /dev/null +++ b/sop-java/src/test/java/sop/ProfileTest.java @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop; + +import org.junit.jupiter.api.Test; +import sop.Profile; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ProfileTest { + + @Test + public void toStringFull() { + Profile profile = new Profile("default", "Use the implementers recommendations."); + assertEquals("default: Use the implementers recommendations.", profile.toString()); + } + + @Test + public void toStringNameOnly() { + Profile profile = new Profile("default"); + assertEquals("default", profile.toString()); + } + + @Test + public void parseFull() { + String string = "default: Use the implementers recommendations."; + Profile profile = Profile.parse(string); + assertEquals("default", profile.getName()); + assertTrue(profile.hasDescription()); + assertEquals("Use the implementers recommendations.", profile.getDescription().get()); + } + + @Test + public void parseNameOnly() { + String string = "rfc4880"; + Profile profile = Profile.parse(string); + assertEquals("rfc4880", profile.getName()); + assertFalse(profile.hasDescription()); + } + + @Test + public void parseEmptyDescription() { + String string = "rfc4880: "; + Profile profile = Profile.parse(string); + assertEquals("rfc4880", profile.getName()); + assertFalse(profile.hasDescription()); + + string = "rfc4880:"; + profile = Profile.parse(string); + assertEquals("rfc4880", profile.getName()); + assertFalse(profile.hasDescription()); + } + + @Test + public void parseTooLongProfile() { + // 1200 chars + String string = "longDescription: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."; + assertThrows(IllegalArgumentException.class, () -> Profile.parse(string)); + } + + @Test + public void constructTooLongProfile() { + // name + description = 1200 chars + String name = "longDescription"; + String description = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."; + assertThrows(IllegalArgumentException.class, () -> new Profile(name, description)); + } + + @Test + public void nameCannotBeEmpty() { + assertThrows(IllegalArgumentException.class, () -> new Profile("")); + assertThrows(IllegalArgumentException.class, () -> new Profile(""), "Description Text."); + } + + @Test + public void nameCannotContainColons() { + assertThrows(IllegalArgumentException.class, () -> new Profile("default:")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default:", "DescriptionText")); + assertThrows(IllegalArgumentException.class, () -> new Profile("rfc:4880")); + assertThrows(IllegalArgumentException.class, () -> new Profile("rfc:4880", "OpenPGP Message Format")); + } + + @Test + public void nameCannotContainWhitespace() { + assertThrows(IllegalArgumentException.class, () -> new Profile("default profile")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default profile", "With description.")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\nprofile")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\nprofile", "With description")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\tprofile")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\tprofile", "With description")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\r\nprofile")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\r\nprofile", "With description")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\rprofile")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\rprofile", "With description")); + } +} From 312cdb69c92e30a41d1fdb059de2f8f0a47c7b93 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 14:28:07 +0200 Subject: [PATCH 036/298] Update changelog --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9bc020..b78eafc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,13 @@ SPDX-License-Identifier: Apache-2.0 ## 6.1.0-SNAPSHOT - `listProfiles()`: Add shortcut methods `generateKey()` and `encrypt()` - Add DSL for testing `Verification` results -- `Verification`: Return `Optional<>` for `getSignatureMode()` and `getDescription()` +- `Verification` + - Return `Optional` for `getSignatureMode()` + - Return `Optional` for `getDescription()` +- `Profile` + - Add support for profiles without description + - Return `Optional` for `getDescription()` + - Add `parse(String)` method for parsing profile lines - `sop-java`: Add dependency on `com.google.code.findbugs:jsr305` for `@Nullable`, `@Nonnull` annotations - `UTCUtil`: `parseUTCDate()` is now `@Nonnull` and throws a `ParseException` for invalid inputs - `UTF8Util`: `decodeUTF8()` now throws `CharacterCodingException` instead of `SOPGPException.PasswordNotHumanReadable` From 419056ba4c391cef4dc23e28e133d96727631bf5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 14:32:13 +0200 Subject: [PATCH 037/298] Fix checkstyle issues --- .../src/test/java/sop/ByteArrayAndResultTest.java | 10 ++++------ sop-java/src/test/java/sop/MicAlgTest.java | 11 +++++------ sop-java/src/test/java/sop/ProfileTest.java | 1 - sop-java/src/test/java/sop/ReadyTest.java | 5 ++--- sop-java/src/test/java/sop/ReadyWithResultTest.java | 13 +++++-------- sop-java/src/test/java/sop/SessionKeyTest.java | 7 +++---- sop-java/src/test/java/sop/SigningResultTest.java | 6 ++---- sop-java/src/test/java/sop/VerificationTest.java | 1 - 8 files changed, 21 insertions(+), 33 deletions(-) diff --git a/sop-java/src/test/java/sop/ByteArrayAndResultTest.java b/sop-java/src/test/java/sop/ByteArrayAndResultTest.java index 8d89842..9f9d9ce 100644 --- a/sop-java/src/test/java/sop/ByteArrayAndResultTest.java +++ b/sop-java/src/test/java/sop/ByteArrayAndResultTest.java @@ -4,18 +4,16 @@ package sop; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; +import sop.util.UTCUtil; import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.Collections; import java.util.List; -import org.junit.jupiter.api.Test; -import sop.ByteArrayAndResult; -import sop.Verification; -import sop.util.UTCUtil; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class ByteArrayAndResultTest { diff --git a/sop-java/src/test/java/sop/MicAlgTest.java b/sop-java/src/test/java/sop/MicAlgTest.java index a2cbf73..16f54ef 100644 --- a/sop-java/src/test/java/sop/MicAlgTest.java +++ b/sop-java/src/test/java/sop/MicAlgTest.java @@ -4,16 +4,15 @@ package sop; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; -import org.junit.jupiter.api.Test; -import sop.MicAlg; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; public class MicAlgTest { diff --git a/sop-java/src/test/java/sop/ProfileTest.java b/sop-java/src/test/java/sop/ProfileTest.java index d76d270..564a6af 100644 --- a/sop-java/src/test/java/sop/ProfileTest.java +++ b/sop-java/src/test/java/sop/ProfileTest.java @@ -5,7 +5,6 @@ package sop; import org.junit.jupiter.api.Test; -import sop.Profile; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; diff --git a/sop-java/src/test/java/sop/ReadyTest.java b/sop-java/src/test/java/sop/ReadyTest.java index 409bfc0..49c297b 100644 --- a/sop-java/src/test/java/sop/ReadyTest.java +++ b/sop-java/src/test/java/sop/ReadyTest.java @@ -4,14 +4,13 @@ package sop; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; -import org.junit.jupiter.api.Test; -import sop.Ready; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; public class ReadyTest { diff --git a/sop-java/src/test/java/sop/ReadyWithResultTest.java b/sop-java/src/test/java/sop/ReadyWithResultTest.java index 072314a..88231dc 100644 --- a/sop-java/src/test/java/sop/ReadyWithResultTest.java +++ b/sop-java/src/test/java/sop/ReadyWithResultTest.java @@ -4,8 +4,9 @@ package sop; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; +import sop.exception.SOPGPException; +import sop.util.UTCUtil; import java.io.IOException; import java.io.OutputStream; @@ -14,12 +15,8 @@ import java.text.ParseException; import java.util.Collections; import java.util.List; -import org.junit.jupiter.api.Test; -import sop.ByteArrayAndResult; -import sop.ReadyWithResult; -import sop.Verification; -import sop.exception.SOPGPException; -import sop.util.UTCUtil; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class ReadyWithResultTest { diff --git a/sop-java/src/test/java/sop/SessionKeyTest.java b/sop-java/src/test/java/sop/SessionKeyTest.java index 7737a29..6dd3c90 100644 --- a/sop-java/src/test/java/sop/SessionKeyTest.java +++ b/sop-java/src/test/java/sop/SessionKeyTest.java @@ -4,14 +4,13 @@ package sop; +import org.junit.jupiter.api.Test; +import sop.util.HexUtil; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import org.junit.jupiter.api.Test; -import sop.SessionKey; -import sop.util.HexUtil; - public class SessionKeyTest { @Test diff --git a/sop-java/src/test/java/sop/SigningResultTest.java b/sop-java/src/test/java/sop/SigningResultTest.java index a2f2d77..b0e1afd 100644 --- a/sop-java/src/test/java/sop/SigningResultTest.java +++ b/sop-java/src/test/java/sop/SigningResultTest.java @@ -4,11 +4,9 @@ package sop; -import static org.junit.jupiter.api.Assertions.assertEquals; - import org.junit.jupiter.api.Test; -import sop.MicAlg; -import sop.SigningResult; + +import static org.junit.jupiter.api.Assertions.assertEquals; public class SigningResultTest { diff --git a/sop-java/src/test/java/sop/VerificationTest.java b/sop-java/src/test/java/sop/VerificationTest.java index c0b0cbc..e956435 100644 --- a/sop-java/src/test/java/sop/VerificationTest.java +++ b/sop-java/src/test/java/sop/VerificationTest.java @@ -5,7 +5,6 @@ package sop; import org.junit.jupiter.api.Test; -import sop.Verification; import sop.enums.SignatureMode; import sop.testsuite.assertions.VerificationAssert; import sop.util.UTCUtil; From ab8f44138d0042fc611e4ee4e14304d626a12acc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 14:45:42 +0200 Subject: [PATCH 038/298] Add missing @throws javadoc --- sop-java/src/main/java/sop/util/UTCUtil.java | 1 + sop-java/src/main/java/sop/util/UTF8Util.java | 1 + 2 files changed, 2 insertions(+) diff --git a/sop-java/src/main/java/sop/util/UTCUtil.java b/sop-java/src/main/java/sop/util/UTCUtil.java index 3849258..96a6b9c 100644 --- a/sop-java/src/main/java/sop/util/UTCUtil.java +++ b/sop-java/src/main/java/sop/util/UTCUtil.java @@ -33,6 +33,7 @@ public class UTCUtil { * * @param dateString string * @return date + * @throws ParseException if the date string is malformed and cannot be parsed */ @Nonnull public static Date parseUTCDate(String dateString) throws ParseException { diff --git a/sop-java/src/main/java/sop/util/UTF8Util.java b/sop-java/src/main/java/sop/util/UTF8Util.java index a59ff43..1b4941b 100644 --- a/sop-java/src/main/java/sop/util/UTF8Util.java +++ b/sop-java/src/main/java/sop/util/UTF8Util.java @@ -26,6 +26,7 @@ public class UTF8Util { * @param data utf-8 encoded bytes * * @return decoded string + * @throws CharacterCodingException if the input data does not resemble UTF8 */ public static String decodeUTF8(byte[] data) throws CharacterCodingException { From b8ad6d77a25a6ab41a5b8c28eb242439ea134a27 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 14:46:52 +0200 Subject: [PATCH 039/298] SOP-Java 6.1.0 --- CHANGELOG.md | 2 +- version.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b78eafc..6d15e9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 6.1.0-SNAPSHOT +## 6.1.0 - `listProfiles()`: Add shortcut methods `generateKey()` and `encrypt()` - Add DSL for testing `Verification` results - `Verification` diff --git a/version.gradle b/version.gradle index 095db6c..092f937 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '6.0.1' - isSnapshot = true + shortVersion = '6.1.0' + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 7ab65f63a4aac669f5acea98c13960adfc34b05e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 27 Apr 2023 14:49:13 +0200 Subject: [PATCH 040/298] SOP-Java 6.1.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 092f937..ca55347 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '6.1.0' - isSnapshot = false + shortVersion = '6.1.1' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From bfaba69222d7c18ef2a2a931c8a57d618b34a323 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Jul 2023 21:18:37 +0200 Subject: [PATCH 041/298] Add Dearmor.data(String) default method --- .../src/main/java/sop/operation/Dearmor.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/sop-java/src/main/java/sop/operation/Dearmor.java b/sop-java/src/main/java/sop/operation/Dearmor.java index 380c4fc..524dc8c 100644 --- a/sop-java/src/main/java/sop/operation/Dearmor.java +++ b/sop-java/src/main/java/sop/operation/Dearmor.java @@ -10,6 +10,7 @@ import java.io.InputStream; import sop.Ready; import sop.exception.SOPGPException; +import sop.util.UTF8Util; public interface Dearmor { @@ -19,7 +20,7 @@ public interface Dearmor { * @param data armored OpenPGP data * @return input stream of unarmored data * - * @throws sop.exception.SOPGPException.BadData in case of non-OpenPGP data + * @throws SOPGPException.BadData in case of non-OpenPGP data * @throws IOException in case of an IO error */ Ready data(InputStream data) @@ -32,7 +33,7 @@ public interface Dearmor { * @param data armored OpenPGP data * @return input stream of unarmored data * - * @throws sop.exception.SOPGPException.BadData in case of non-OpenPGP data + * @throws SOPGPException.BadData in case of non-OpenPGP data * @throws IOException in case of an IO error */ default Ready data(byte[] data) @@ -40,4 +41,19 @@ public interface Dearmor { IOException { return data(new ByteArrayInputStream(data)); } + + /** + * Dearmor amored OpenPGP data. + * + * @param data armored OpenPGP data + * @return input stream of unarmored data + * + * @throws SOPGPException.BadData in case of non-OpenPGP data + * @throws IOException in case of an IO error + */ + default Ready data(String data) + throws SOPGPException.BadData, + IOException { + return data(data.getBytes(UTF8Util.UTF8)); + } } From f65ddba4b40786268ed065770879303eb3bd4cf2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Jul 2023 21:27:51 +0200 Subject: [PATCH 042/298] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d15e9a..ec9e7d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 6.1.1-SNAPSHOT +- Add `dearmor.data(String)` utility method + ## 6.1.0 - `listProfiles()`: Add shortcut methods `generateKey()` and `encrypt()` - Add DSL for testing `Verification` results From ab2e4aa8e7b0cd5b000aeb05b1ffd1413a90babc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Jul 2023 22:41:58 +0200 Subject: [PATCH 043/298] Bump version to 7.0.0-SNAPSHOT --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index ca55347..7614d8f 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '6.1.1' + shortVersion = '7.0.0' isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 From e6393b44b984a83a020daa17c99c29e7dcc26d85 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Jul 2023 22:42:16 +0200 Subject: [PATCH 044/298] Initial implementation of 'revoke-key' command --- .../main/java/sop/external/ExternalSOP.java | 6 ++ .../src/main/java/sop/cli/picocli/SopCLI.java | 2 + .../cli/picocli/commands/RevokeKeyCmd.java | 62 +++++++++++++++++++ .../main/resources/msg_revoke-key.properties | 9 +++ .../resources/msg_revoke-key_de.properties | 9 +++ .../test/java/sop/cli/picocli/SOPTest.java | 6 ++ sop-java/src/main/java/sop/SOP.java | 8 +++ .../main/java/sop/operation/RevokeKey.java | 49 +++++++++++++++ 8 files changed, 151 insertions(+) create mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java create mode 100644 sop-java-picocli/src/main/resources/msg_revoke-key.properties create mode 100644 sop-java-picocli/src/main/resources/msg_revoke-key_de.properties create mode 100644 sop-java/src/main/java/sop/operation/RevokeKey.java diff --git a/external-sop/src/main/java/sop/external/ExternalSOP.java b/external-sop/src/main/java/sop/external/ExternalSOP.java index bc5eb46..189b648 100644 --- a/external-sop/src/main/java/sop/external/ExternalSOP.java +++ b/external-sop/src/main/java/sop/external/ExternalSOP.java @@ -32,6 +32,7 @@ import sop.operation.InlineDetach; import sop.operation.InlineSign; import sop.operation.InlineVerify; import sop.operation.ListProfiles; +import sop.operation.RevokeKey; import sop.operation.Version; import javax.annotation.Nonnull; @@ -161,6 +162,11 @@ public class ExternalSOP implements SOP { return new ListProfilesExternal(binaryName, properties); } + @Override + public RevokeKey revokeKey() { + return null; + } + @Override public Dearmor dearmor() { return new DearmorExternal(binaryName, properties); diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java index fab99e0..2f98061 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java @@ -17,6 +17,7 @@ import sop.cli.picocli.commands.GenerateKeyCmd; import sop.cli.picocli.commands.InlineSignCmd; import sop.cli.picocli.commands.InlineVerifyCmd; import sop.cli.picocli.commands.ListProfilesCmd; +import sop.cli.picocli.commands.RevokeKeyCmd; import sop.cli.picocli.commands.SignCmd; import sop.cli.picocli.commands.VerifyCmd; import sop.cli.picocli.commands.VersionCmd; @@ -43,6 +44,7 @@ import java.util.ResourceBundle; InlineSignCmd.class, InlineVerifyCmd.class, ListProfilesCmd.class, + RevokeKeyCmd.class, VersionCmd.class, AutoComplete.GenerateCompletion.class } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java new file mode 100644 index 0000000..28d1890 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands; + +import picocli.CommandLine; +import sop.Ready; +import sop.cli.picocli.SopCLI; +import sop.exception.SOPGPException; +import sop.operation.RevokeKey; + +import java.io.IOException; + +@CommandLine.Command(name = "revoke-key", + resourceBundle = "msg_revoke-key", + exitCodeOnInvalidInput = 37) +public class RevokeKeyCmd extends AbstractSopCmd { + + @CommandLine.Option(names = "--no-armor", + negatable = true) + boolean armor = true; + + @CommandLine.Option(names = "--with-key-password", + paramLabel = "PASSWORD") + String withKeyPassword; + + @Override + public void run() { + RevokeKey revokeKey = throwIfUnsupportedSubcommand( + SopCLI.getSop().revokeKey(), "revoke-key"); + + if (!armor) { + revokeKey.noArmor(); + } + + if (withKeyPassword != null) { + try { + String password = stringFromInputStream(getInput(withKeyPassword)); + revokeKey.withKeyPassword(password); + } catch (SOPGPException.UnsupportedOption e) { + String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); + throw new SOPGPException.UnsupportedOption(errorMsg, e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + Ready ready; + try { + ready = revokeKey.keys(System.in); + } catch (SOPGPException.KeyIsProtected e) { + String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", "STANDARD_IN"); + throw new SOPGPException.KeyIsProtected(errorMsg, e); + } + try { + ready.writeTo(System.out); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/sop-java-picocli/src/main/resources/msg_revoke-key.properties b/sop-java-picocli/src/main/resources/msg_revoke-key.properties new file mode 100644 index 0000000..e90cd48 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_revoke-key.properties @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2023 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Generate revocation certificate for private keys +# Generic TODO: Remove when bumping picocli to 4.7.0 +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_revoke-key_de.properties b/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties new file mode 100644 index 0000000..f073213 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2023 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Erzeuge Widerrufszertifikate für private Schlüssel +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.synopsisHeading=Aufruf:\u0020 +usage.commandListHeading=%nBefehle:%n +usage.optionListHeading = %nOptionen:%n +usage.footerHeading=Powered by Picocli%n 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 c05440e..67a951c 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 @@ -27,6 +27,7 @@ import sop.operation.InlineVerify; import sop.operation.DetachedSign; import sop.operation.DetachedVerify; import sop.operation.ListProfiles; +import sop.operation.RevokeKey; import sop.operation.Version; public class SOPTest { @@ -101,6 +102,11 @@ public class SOPTest { return null; } + @Override + public RevokeKey revokeKey() { + return null; + } + @Override public InlineDetach inlineDetach() { return null; diff --git a/sop-java/src/main/java/sop/SOP.java b/sop-java/src/main/java/sop/SOP.java index 804cd28..460e6c1 100644 --- a/sop-java/src/main/java/sop/SOP.java +++ b/sop-java/src/main/java/sop/SOP.java @@ -16,6 +16,7 @@ import sop.operation.InlineVerify; import sop.operation.DetachedSign; import sop.operation.DetachedVerify; import sop.operation.ListProfiles; +import sop.operation.RevokeKey; import sop.operation.Version; /** @@ -158,4 +159,11 @@ public interface SOP { * @return builder instance */ ListProfiles listProfiles(); + + /** + * Revoke one or more secret keys. + * + * @return builder instance + */ + RevokeKey revokeKey(); } diff --git a/sop-java/src/main/java/sop/operation/RevokeKey.java b/sop-java/src/main/java/sop/operation/RevokeKey.java new file mode 100644 index 0000000..e2aa433 --- /dev/null +++ b/sop-java/src/main/java/sop/operation/RevokeKey.java @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2021 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.InputStream; + +public interface RevokeKey { + + /** + * Disable ASCII armor encoding. + * + * @return builder instance + */ + RevokeKey noArmor(); + + /** + * Provide the decryption password for the secret key. + * + * @param password password + * @return builder instance + * @throws SOPGPException.UnsupportedOption if the implementation does not support key passwords + * @throws SOPGPException.PasswordNotHumanReadable if the password is not human-readable + */ + default RevokeKey withKeyPassword(String password) + throws SOPGPException.UnsupportedOption, + SOPGPException.PasswordNotHumanReadable { + return withKeyPassword(password.getBytes(UTF8Util.UTF8)); + } + + /** + * Provide the decryption password for the secret key. + * + * @param password password + * @return builder instance + * @throws SOPGPException.UnsupportedOption if the implementation does not support key passwords + * @throws SOPGPException.PasswordNotHumanReadable if the password is not human-readable + */ + RevokeKey withKeyPassword(byte[] password) + throws SOPGPException.UnsupportedOption, + SOPGPException.PasswordNotHumanReadable; + + Ready keys(InputStream keys); +} From 618d123a7beaf0b448afee6835851c07a7395997 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Jul 2023 22:58:39 +0200 Subject: [PATCH 045/298] Add RevokeKeyExternal implementation and some basic tests --- .../main/java/sop/external/ExternalSOP.java | 3 +- .../external/operation/RevokeKeyExternal.java | 48 ++++++++++++ .../operation/ExternalRevokeKeyTest.java | 11 +++ .../main/java/sop/operation/RevokeKey.java | 5 ++ .../testsuite/operation/RevokeKeyTest.java | 77 +++++++++++++++++++ 5 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java create mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java create mode 100644 sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java diff --git a/external-sop/src/main/java/sop/external/ExternalSOP.java b/external-sop/src/main/java/sop/external/ExternalSOP.java index 189b648..c3041ae 100644 --- a/external-sop/src/main/java/sop/external/ExternalSOP.java +++ b/external-sop/src/main/java/sop/external/ExternalSOP.java @@ -19,6 +19,7 @@ import sop.external.operation.InlineDetachExternal; import sop.external.operation.InlineSignExternal; import sop.external.operation.InlineVerifyExternal; import sop.external.operation.ListProfilesExternal; +import sop.external.operation.RevokeKeyExternal; import sop.external.operation.VersionExternal; import sop.operation.Armor; import sop.operation.Dearmor; @@ -164,7 +165,7 @@ public class ExternalSOP implements SOP { @Override public RevokeKey revokeKey() { - return null; + return new RevokeKeyExternal(binaryName, properties); } @Override diff --git a/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java b/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java new file mode 100644 index 0000000..e7aad9d --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation; + +import sop.Ready; +import sop.exception.SOPGPException; +import sop.external.ExternalSOP; +import sop.operation.RevokeKey; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +public class RevokeKeyExternal implements RevokeKey { + + private final List commandList = new ArrayList<>(); + private final List envList; + + private int withKeyPasswordCounter = 0; + + public RevokeKeyExternal(String binary, Properties environment) { + this.commandList.add(binary); + this.commandList.add("revoke-key"); + this.envList = ExternalSOP.propertiesToEnv(environment); + } + + @Override + public RevokeKey noArmor() { + this.commandList.add("--no-armor"); + return this; + } + + @Override + public RevokeKey withKeyPassword(byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { + String envVar = "KEY_PASSWORD_" + withKeyPasswordCounter++; + commandList.add("--with-key-password=@ENV:" + envVar); + envList.add(envVar + "=" + new String(password)); + return this; + } + + @Override + public Ready keys(InputStream keys) { + return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys); + } +} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java new file mode 100644 index 0000000..2318abd --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.external.operation; + +import sop.testsuite.operation.RevokeKeyTest; + +public class ExternalRevokeKeyTest extends RevokeKeyTest { + +} diff --git a/sop-java/src/main/java/sop/operation/RevokeKey.java b/sop-java/src/main/java/sop/operation/RevokeKey.java index e2aa433..3ceb5b3 100644 --- a/sop-java/src/main/java/sop/operation/RevokeKey.java +++ b/sop-java/src/main/java/sop/operation/RevokeKey.java @@ -8,6 +8,7 @@ import sop.Ready; import sop.exception.SOPGPException; import sop.util.UTF8Util; +import java.io.ByteArrayInputStream; import java.io.InputStream; public interface RevokeKey { @@ -45,5 +46,9 @@ public interface RevokeKey { throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable; + default Ready keys(byte[] bytes) { + return keys(new ByteArrayInputStream(bytes)); + } + Ready keys(InputStream keys); } diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java new file mode 100644 index 0000000..920f877 --- /dev/null +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +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 sop.SOP; +import sop.exception.SOPGPException; +import sop.util.UTF8Util; + +import java.io.IOException; +import java.util.Arrays; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class RevokeKeyTest extends AbstractSOPTest { + + static Stream provideInstances() { + return provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void revokeUnprotectedKey(SOP sop) throws IOException { + byte[] secretKey = sop.generateKey().userId("Alice ").generate().getBytes(); + byte[] revocation = sop.revokeKey().keys(secretKey).getBytes(); + + assertFalse(Arrays.equals(secretKey, revocation)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void revokeProtectedKey(SOP sop) throws IOException { + byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice ").generate().getBytes(); + byte[] revocation = sop.revokeKey().withKeyPassword(password).keys(secretKey).getBytes(); + + assertFalse(Arrays.equals(secretKey, revocation)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void revokeProtectedKeyWithMultiplePasswordOptions(SOP sop) throws IOException { + byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + byte[] wrongPassword = "0r4ng3".getBytes(UTF8Util.UTF8); + byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice ").generate().getBytes(); + byte[] revocation = sop.revokeKey().withKeyPassword(wrongPassword).withKeyPassword(password).keys(secretKey).getBytes(); + + assertFalse(Arrays.equals(secretKey, revocation)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void revokeProtectedKeyWithMissingPassphraseFails(SOP sop) throws IOException { + byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice ").generate().getBytes(); + + assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.revokeKey().keys(secretKey).getBytes()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void revokeProtectedKeyWithWrongPassphraseFails(SOP sop) throws IOException { + byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + String wrongPassword = "or4ng3"; + byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice ").generate().getBytes(); + + assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.revokeKey().withKeyPassword(wrongPassword).keys(secretKey).getBytes()); + } +} From 7e1377a28cb15eb42d2386757346f9f8688eb405 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 00:42:02 +0200 Subject: [PATCH 046/298] Initial implementation of 'change-key-password' command of SOP-07 --- .../main/java/sop/external/ExternalSOP.java | 7 ++ .../operation/ChangeKeyPasswordExternal.java | 57 +++++++++++ .../operation/ExternalRevokeKeyTest.java | 2 + .../src/main/java/sop/cli/picocli/SopCLI.java | 2 + .../commands/ChangeKeyPasswordCmd.java | 54 ++++++++++ .../test/java/sop/cli/picocli/SOPTest.java | 6 ++ sop-java/src/main/java/sop/SOP.java | 8 ++ .../java/sop/operation/ChangeKeyPassword.java | 83 ++++++++++++++++ .../operation/ChangeKeyPasswordTest.java | 98 +++++++++++++++++++ 9 files changed, 317 insertions(+) create mode 100644 external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java create mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java create mode 100644 sop-java/src/main/java/sop/operation/ChangeKeyPassword.java create mode 100644 sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java diff --git a/external-sop/src/main/java/sop/external/ExternalSOP.java b/external-sop/src/main/java/sop/external/ExternalSOP.java index c3041ae..376e54c 100644 --- a/external-sop/src/main/java/sop/external/ExternalSOP.java +++ b/external-sop/src/main/java/sop/external/ExternalSOP.java @@ -8,6 +8,7 @@ import sop.Ready; import sop.SOP; import sop.exception.SOPGPException; import sop.external.operation.ArmorExternal; +import sop.external.operation.ChangeKeyPasswordExternal; import sop.external.operation.DearmorExternal; import sop.external.operation.DecryptExternal; import sop.external.operation.DetachedSignExternal; @@ -22,6 +23,7 @@ import sop.external.operation.ListProfilesExternal; import sop.external.operation.RevokeKeyExternal; import sop.external.operation.VersionExternal; import sop.operation.Armor; +import sop.operation.ChangeKeyPassword; import sop.operation.Dearmor; import sop.operation.Decrypt; import sop.operation.DetachedSign; @@ -168,6 +170,11 @@ public class ExternalSOP implements SOP { return new RevokeKeyExternal(binaryName, properties); } + @Override + public ChangeKeyPassword changeKeyPassword() { + return new ChangeKeyPasswordExternal(binaryName, properties); + } + @Override public Dearmor dearmor() { return new DearmorExternal(binaryName, properties); diff --git a/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java b/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java new file mode 100644 index 0000000..210152f --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation; + +import sop.Ready; +import sop.exception.SOPGPException; +import sop.external.ExternalSOP; +import sop.operation.ChangeKeyPassword; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +public class ChangeKeyPasswordExternal implements ChangeKeyPassword { + private final List commandList = new ArrayList<>(); + private final List envList; + + private int keyPasswordCounter = 0; + + public ChangeKeyPasswordExternal(String binary, Properties environment) { + this.commandList.add(binary); + this.commandList.add("decrypt"); + this.envList = ExternalSOP.propertiesToEnv(environment); + } + + @Override + public ChangeKeyPassword noArmor() { + this.commandList.add("--no-armor"); + return this; + } + + @Override + public ChangeKeyPassword oldKeyPassphrase(String oldPassphrase) { + this.commandList.add("--old-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter); + this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + oldPassphrase); + keyPasswordCounter++; + + return this; + } + + @Override + public ChangeKeyPassword newKeyPassphrase(String newPassphrase) { + this.commandList.add("--new-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter); + this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + newPassphrase); + keyPasswordCounter++; + + return this; + } + + @Override + public Ready keys(InputStream inputStream) throws SOPGPException.KeyIsProtected, SOPGPException.BadData { + return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, inputStream); + } +} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java index 2318abd..e2efe03 100644 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java @@ -4,8 +4,10 @@ package sop.testsuite.external.operation; +import org.junit.jupiter.api.condition.EnabledIf; import sop.testsuite.operation.RevokeKeyTest; +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") public class ExternalRevokeKeyTest extends RevokeKeyTest { } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java index 2f98061..e80c887 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java @@ -8,6 +8,7 @@ import picocli.AutoComplete; import picocli.CommandLine; import sop.SOP; import sop.cli.picocli.commands.ArmorCmd; +import sop.cli.picocli.commands.ChangeKeyPasswordCmd; import sop.cli.picocli.commands.DearmorCmd; import sop.cli.picocli.commands.DecryptCmd; import sop.cli.picocli.commands.InlineDetachCmd; @@ -45,6 +46,7 @@ import java.util.ResourceBundle; InlineVerifyCmd.class, ListProfilesCmd.class, RevokeKeyCmd.class, + ChangeKeyPasswordCmd.class, VersionCmd.class, AutoComplete.GenerateCompletion.class } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java new file mode 100644 index 0000000..2f44ec7 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands; + +import picocli.CommandLine; +import sop.cli.picocli.SopCLI; +import sop.exception.SOPGPException; +import sop.operation.ChangeKeyPassword; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +@CommandLine.Command(name = "change-key-password", + resourceBundle = "msg_dearmor", // TODO + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +public class ChangeKeyPasswordCmd extends AbstractSopCmd { + + @CommandLine.Option(names = "--no-armor", + negatable = true) + boolean armor = true; + + @CommandLine.Option(names = {"--old-key-password"}) + List oldKeyPasswords = new ArrayList<>(); + + @CommandLine.Option(names = {"--new-key-password"}, arity = "0..1") + String newKeyPassword = null; + + @Override + public void run() { + ChangeKeyPassword changeKeyPassword = throwIfUnsupportedSubcommand( + SopCLI.getSop().changeKeyPassword(), "change-key-password"); + + if (!armor) { + changeKeyPassword.noArmor(); + } + + for (String oldKeyPassword : oldKeyPasswords) { + changeKeyPassword.oldKeyPassphrase(oldKeyPassword); + } + + if (newKeyPassword != null) { + changeKeyPassword.newKeyPassphrase(newKeyPassword); + } + + try { + changeKeyPassword.keys(System.in).writeTo(System.out); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} 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 67a951c..47b6123 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 @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test; import sop.SOP; import sop.exception.SOPGPException; import sop.operation.Armor; +import sop.operation.ChangeKeyPassword; import sop.operation.Dearmor; import sop.operation.Decrypt; import sop.operation.InlineDetach; @@ -107,6 +108,11 @@ public class SOPTest { return null; } + @Override + public ChangeKeyPassword changeKeyPassword() { + return null; + } + @Override public InlineDetach inlineDetach() { return null; diff --git a/sop-java/src/main/java/sop/SOP.java b/sop-java/src/main/java/sop/SOP.java index 460e6c1..1200e21 100644 --- a/sop-java/src/main/java/sop/SOP.java +++ b/sop-java/src/main/java/sop/SOP.java @@ -5,6 +5,7 @@ package sop; import sop.operation.Armor; +import sop.operation.ChangeKeyPassword; import sop.operation.Dearmor; import sop.operation.Decrypt; import sop.operation.Encrypt; @@ -166,4 +167,11 @@ public interface SOP { * @return builder instance */ RevokeKey revokeKey(); + + /** + * Update a key's password. + * + * @return builder instance + */ + ChangeKeyPassword changeKeyPassword(); } diff --git a/sop-java/src/main/java/sop/operation/ChangeKeyPassword.java b/sop-java/src/main/java/sop/operation/ChangeKeyPassword.java new file mode 100644 index 0000000..460a20a --- /dev/null +++ b/sop-java/src/main/java/sop/operation/ChangeKeyPassword.java @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2023 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.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.CharacterCodingException; + +public interface ChangeKeyPassword { + + /** + * Disable ASCII armoring of the output. + * + * @return builder instance + */ + ChangeKeyPassword noArmor(); + + default ChangeKeyPassword oldKeyPassphrase(byte[] password) { + try { + return oldKeyPassphrase(UTF8Util.decodeUTF8(password)); + } catch (CharacterCodingException e) { + throw new SOPGPException.PasswordNotHumanReadable("Password MUST be a valid UTF8 string."); + } + } + + /** + * Provide a passphrase to unlock the secret key. + * This method can be provided multiple times to provide separate passphrases that are tried as a + * means to unlock any secret key material encountered. + * + * @param oldPassphrase old passphrase + * @return builder instance + */ + ChangeKeyPassword oldKeyPassphrase(String oldPassphrase); + + /** + * Provide a passphrase to re-lock the secret key with. + * This method can only be used once, and all key material encountered will be encrypted with the given passphrase. + * If this method is not called, the key material will not be protected. + * + * @param newPassphrase new passphrase + * @return builder instance + */ + default ChangeKeyPassword newKeyPassphrase(byte[] newPassphrase) { + try { + return newKeyPassphrase(UTF8Util.decodeUTF8(newPassphrase)); + } catch (CharacterCodingException e) { + throw new SOPGPException.PasswordNotHumanReadable("Password MUST be a valid UTF8 string."); + } + } + + /** + * Provide a passphrase to re-lock the secret key with. + * This method can only be used once, and all key material encountered will be encrypted with the given passphrase. + * If this method is not called, the key material will not be protected. + * + * @param newPassphrase new passphrase + * @return builder instance + */ + ChangeKeyPassword newKeyPassphrase(String newPassphrase); + + default Ready keys(byte[] keys) throws SOPGPException.KeyIsProtected, SOPGPException.BadData { + return keys(new ByteArrayInputStream(keys)); + } + + /** + * Provide the key material. + * + * @param inputStream input stream of secret key material + * @return ready + * + * @throws sop.exception.SOPGPException.KeyIsProtected if any (sub-) key encountered cannot be unlocked. + * @throws sop.exception.SOPGPException.BadData if the key material is malformed + */ + Ready keys(InputStream inputStream) throws SOPGPException.KeyIsProtected, SOPGPException.BadData; + +} diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java new file mode 100644 index 0000000..b575047 --- /dev/null +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +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 sop.SOP; +import sop.exception.SOPGPException; +import sop.util.UTF8Util; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class ChangeKeyPasswordTest extends AbstractSOPTest { + + static Stream provideInstances() { + return AbstractSOPTest.provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void changePasswordFromUnprotectedToProtected(SOP sop) throws IOException { + byte[] unprotectedKey = sop.generateKey().generate().getBytes(); + byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + byte[] protectedKey = sop.changeKeyPassword().newKeyPassphrase(password).keys(unprotectedKey).getBytes(); + + sop.sign().withKeyPassword(password).key(protectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void changePasswordFromUnprotectedToUnprotected(SOP sop) throws IOException { + byte[] unprotectedKey = sop.generateKey().noArmor().generate().getBytes(); + byte[] stillUnprotectedKey = sop.changeKeyPassword().noArmor().keys(unprotectedKey).getBytes(); + + assertArrayEquals(unprotectedKey, stillUnprotectedKey); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void changePasswordFromProtectedToUnprotected(SOP sop) throws IOException { + byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + byte[] protectedKey = sop.generateKey().withKeyPassword(password).generate().getBytes(); + byte[] unprotectedKey = sop.changeKeyPassword() + .oldKeyPassphrase(password) + .keys(protectedKey).getBytes(); + + sop.sign().key(unprotectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void changePasswordFromProtectedToDifferentProtected(SOP sop) throws IOException { + byte[] oldPassword = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + byte[] newPassword = "0r4ng3".getBytes(UTF8Util.UTF8); + byte[] protectedKey = sop.generateKey().withKeyPassword(oldPassword).generate().getBytes(); + byte[] reprotectedKey = sop.changeKeyPassword() + .oldKeyPassphrase(oldPassword) + .newKeyPassphrase(newPassword) + .keys(protectedKey).getBytes(); + + sop.sign().key(reprotectedKey).withKeyPassword(newPassword).data("Test123".getBytes(StandardCharsets.UTF_8)); + } + + + @ParameterizedTest + @MethodSource("provideInstances") + public void changePasswordWithWrongOldPasswordFails(SOP sop) throws IOException { + byte[] oldPassword = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + byte[] newPassword = "monkey123".getBytes(UTF8Util.UTF8); + byte[] wrongPassword = "0r4ng3".getBytes(UTF8Util.UTF8); + + byte[] protectedKey = sop.generateKey().withKeyPassword(oldPassword).generate().getBytes(); + assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.changeKeyPassword() + .oldKeyPassphrase(wrongPassword) + .newKeyPassphrase(newPassword) + .keys(protectedKey).getBytes()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void nonUtf8PasswordsFail(SOP sop) { + assertThrows(SOPGPException.PasswordNotHumanReadable.class, () -> + sop.changeKeyPassword().oldKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); + assertThrows(SOPGPException.PasswordNotHumanReadable.class, () -> + sop.changeKeyPassword().newKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); + + } +} From 6afe6896d8a8d9cef6dbc79e38dd461c0662a33f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 01:06:41 +0200 Subject: [PATCH 047/298] Implement '--signing-only' option for 'generate-key' command --- .../operation/GenerateKeyExternal.java | 6 ++++++ .../cli/picocli/commands/GenerateKeyCmd.java | 7 +++++++ .../main/java/sop/operation/GenerateKey.java | 7 +++++++ .../testsuite/operation/GenerateKeyTest.java | 21 +++++++++++++++++++ 4 files changed, 41 insertions(+) diff --git a/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java b/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java index e0ca97e..c46dfb3 100644 --- a/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java +++ b/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java @@ -57,6 +57,12 @@ public class GenerateKeyExternal implements GenerateKey { return this; } + @Override + public GenerateKey signingOnly() { + commandList.add("--signing-only"); + return this; + } + @Override public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java index aac7124..a41f086 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java @@ -34,6 +34,9 @@ public class GenerateKeyCmd extends AbstractSopCmd { paramLabel = "PROFILE") String profile; + @CommandLine.Option(names = "--signing-only") + boolean signingOnly = false; + @Override public void run() { GenerateKey generateKey = throwIfUnsupportedSubcommand( @@ -48,6 +51,10 @@ public class GenerateKeyCmd extends AbstractSopCmd { } } + if (signingOnly) { + generateKey.signingOnly(); + } + for (String userId : userId) { generateKey.userId(userId); } diff --git a/sop-java/src/main/java/sop/operation/GenerateKey.java b/sop-java/src/main/java/sop/operation/GenerateKey.java index b788785..77afea1 100644 --- a/sop-java/src/main/java/sop/operation/GenerateKey.java +++ b/sop-java/src/main/java/sop/operation/GenerateKey.java @@ -80,6 +80,13 @@ public interface GenerateKey { */ GenerateKey profile(String profile); + /** + * If this options is set, the generated key will not be capable of encryption / decryption. + * + * @return builder instance + */ + GenerateKey signingOnly(); + /** * Generate the OpenPGP key and return it encoded as an {@link InputStream}. * diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/GenerateKeyTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/GenerateKeyTest.java index ce10763..4a5da58 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/GenerateKeyTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/GenerateKeyTest.java @@ -10,12 +10,16 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import sop.SOP; +import sop.exception.SOPGPException; import sop.testsuite.JUtils; import sop.testsuite.TestData; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.assertThrows; + @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") public class GenerateKeyTest extends AbstractSOPTest { @@ -97,4 +101,21 @@ public class GenerateKeyTest extends AbstractSOPTest { JUtils.assertArrayStartsWith(key, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); JUtils.assertArrayEndsWithIgnoreNewlines(key, TestData.END_PGP_PRIVATE_KEY_BLOCK); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void generateSigningOnlyKey(SOP sop) throws IOException { + byte[] signingOnlyKey = sop.generateKey() + .signingOnly() + .userId("Alice ") + .generate() + .getBytes(); + byte[] signingOnlyCert = sop.extractCert() + .key(signingOnlyKey) + .getBytes(); + + assertThrows(SOPGPException.CertCannotEncrypt.class, () -> + sop.encrypt().withCert(signingOnlyCert) + .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8))); + } } From 9ba005f7cc648f2da76d0db294ba2788966af9da Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 01:10:47 +0200 Subject: [PATCH 048/298] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec9e7d9..c7c3476 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 7.0.0-SNAPSHOT +- 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 + - Add support for new `change-key-password` subcommand + - Add support for new `--signing-only` option of `generate-key` subcommand + ## 6.1.1-SNAPSHOT - Add `dearmor.data(String)` utility method From 6a579bff032b41068558b0efbe310563e70a6ba4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 01:13:01 +0200 Subject: [PATCH 049/298] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d130a48..6dd9a81 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: 6](https://img.shields.io/badge/Spec%20Revision-6-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/06/) +[![Spec Revision: 7](https://img.shields.io/badge/Spec%20Revision-7-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/07/) [![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 d1c614344c0b767139b77c3fd62e6e759606fa93 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 01:35:25 +0200 Subject: [PATCH 050/298] Update i18n --- .../picocli/commands/ChangeKeyPasswordCmd.java | 2 +- .../resources/msg_change-key-password.properties | 15 +++++++++++++++ .../msg_change-key-password_de.properties | 15 +++++++++++++++ .../main/resources/msg_generate-key.properties | 1 + .../main/resources/msg_generate-key_de.properties | 1 + .../src/main/resources/msg_revoke-key.properties | 1 + .../main/resources/msg_revoke-key_de.properties | 1 + 7 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 sop-java-picocli/src/main/resources/msg_change-key-password.properties create mode 100644 sop-java-picocli/src/main/resources/msg_change-key-password_de.properties diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java index 2f44ec7..04e0d3a 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java @@ -14,7 +14,7 @@ import java.util.ArrayList; import java.util.List; @CommandLine.Command(name = "change-key-password", - resourceBundle = "msg_dearmor", // TODO + resourceBundle = "msg_change-key-password", exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) public class ChangeKeyPasswordCmd extends AbstractSopCmd { 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 new file mode 100644 index 0000000..effa2d1 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_change-key-password.properties @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2023 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Update a key's password +no-armor=ASCII armor the output +new-key-password.0=New password to lock the key(s) with. +new-key-password.1=If no new password is given, the result will be unprotected. +old-key-password.0=Old passwords to unlock the key(s) with. +old-key-password.1=You can provide more than one old password. + +# Generic TODO: Remove when bumping picocli to 4.7.0 +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_change-key-password_de.properties b/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties new file mode 100644 index 0000000..17b829d --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2023 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Ändere das Passwort eines Schlüssels +no-armor=Schütze Ausgabe mit ASCII Armor +new-key-password.0=Neues Passwort zur Sicherung des Schlüssels. +new-key-password.1=Falls kein neues Passwort angegeben wird, wird der Schlüssel ungesichert ausgegeben. +old-key-password.0=Altes Passwort zum Entsichern des Schlüssels. +old-key-password.1=Es kann mehr als ein altes Passwort angegeben werden. + +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.synopsisHeading=Aufruf:\u0020 +usage.commandListHeading=%nBefehle:%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 4647791..e38bb65 100644 --- a/sop-java-picocli/src/main/resources/msg_generate-key.properties +++ b/sop-java-picocli/src/main/resources/msg_generate-key.properties @@ -5,6 +5,7 @@ usage.header=Generate a secret key no-armor=ASCII armor the output USERID[0..*]=User-ID, e.g. "Alice " profile=Profile identifier to switch between profiles +signing-only=Generate a key that is not capable of encryption 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...). 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 41a493b..c5f98e0 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 @@ -5,6 +5,7 @@ usage.header=Generiere einen privaten Schl no-armor=Schütze Ausgabe mit ASCII Armor USERID[0..*]=Nutzer-ID, z.B.. "Alice " profile=Profil-Identifikator um zwischen Profilen zu wechseln +signing-only=Generiere einen Schlüssel, der nicht Verschlüsseln kann with-key-password.0=Passwort zum Schutz des privaten Schlüssels with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). 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 e90cd48..d3e59cc 100644 --- a/sop-java-picocli/src/main/resources/msg_revoke-key.properties +++ b/sop-java-picocli/src/main/resources/msg_revoke-key.properties @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 usage.header=Generate revocation certificate for private keys +no-armor=ASCII armor the output # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 usage.commandListHeading = %nCommands:%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 f073213..2f7e294 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 @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 usage.header=Erzeuge Widerrufszertifikate für private Schlüssel +no-armor=Schütze Ausgabe mit ASCII Armor # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n From f13aade98ed09988acb29a71b38b9b58004eff5d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 14:15:47 +0200 Subject: [PATCH 051/298] Add missing parameter labels --- .../java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java | 6 ++++-- .../main/java/sop/cli/picocli/commands/InlineVerifyCmd.java | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java index 04e0d3a..5a6aa2a 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java @@ -22,10 +22,12 @@ public class ChangeKeyPasswordCmd extends AbstractSopCmd { negatable = true) boolean armor = true; - @CommandLine.Option(names = {"--old-key-password"}) + @CommandLine.Option(names = {"--old-key-password"}, + paramLabel = "PASSWORD") List oldKeyPasswords = new ArrayList<>(); - @CommandLine.Option(names = {"--new-key-password"}, arity = "0..1") + @CommandLine.Option(names = {"--new-key-password"}, arity = "0..1", + paramLabel = "PASSWORD") String newKeyPassword = null; @Override diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java index 38ad975..c3b73c5 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java @@ -35,7 +35,7 @@ public class InlineVerifyCmd extends AbstractSopCmd { paramLabel = "DATE") String notAfter = "now"; - @CommandLine.Option(names = "--verifications-out") + @CommandLine.Option(names = "--verifications-out", paramLabel = "VERIFICATIONS") String verificationsOut; @Override From 009364b21794afa39eceda633ae0ee254bd5efbf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 14:17:28 +0200 Subject: [PATCH 052/298] Add missing i18n and fix broken strings --- .../src/main/resources/msg_armor.properties | 1 + .../src/main/resources/msg_armor_de.properties | 1 + .../resources/msg_change-key-password.properties | 14 ++++++++++---- .../msg_change-key-password_de.properties | 14 ++++++++++---- .../src/main/resources/msg_dearmor.properties | 1 + .../src/main/resources/msg_dearmor_de.properties | 1 + .../src/main/resources/msg_decrypt.properties | 16 +++++++++------- .../src/main/resources/msg_decrypt_de.properties | 16 +++++++++------- .../main/resources/msg_detached-sign.properties | 2 ++ .../resources/msg_detached-sign_de.properties | 2 ++ .../resources/msg_detached-verify.properties | 4 +++- .../resources/msg_detached-verify_de.properties | 4 +++- .../src/main/resources/msg_encrypt.properties | 2 ++ .../src/main/resources/msg_encrypt_de.properties | 2 ++ .../main/resources/msg_extract-cert.properties | 1 + .../resources/msg_extract-cert_de.properties | 1 + .../main/resources/msg_generate-key.properties | 4 +++- .../resources/msg_generate-key_de.properties | 4 +++- .../src/main/resources/msg_help.properties | 1 + .../src/main/resources/msg_help_de.properties | 1 + .../main/resources/msg_inline-detach.properties | 1 + .../resources/msg_inline-detach_de.properties | 1 + .../main/resources/msg_inline-sign.properties | 2 ++ .../main/resources/msg_inline-sign_de.properties | 2 ++ .../main/resources/msg_inline-verify.properties | 2 ++ .../resources/msg_inline-verify_de.properties | 2 ++ .../main/resources/msg_list-profiles.properties | 3 +++ .../resources/msg_list-profiles_de.properties | 5 ++++- .../src/main/resources/msg_revoke-key.properties | 8 +++++++- .../main/resources/msg_revoke-key_de.properties | 8 +++++++- .../src/main/resources/msg_version.properties | 1 + .../src/main/resources/msg_version_de.properties | 1 + 32 files changed, 99 insertions(+), 29 deletions(-) diff --git a/sop-java-picocli/src/main/resources/msg_armor.properties b/sop-java-picocli/src/main/resources/msg_armor.properties index 74feb8b..2f4e217 100644 --- a/sop-java-picocli/src/main/resources/msg_armor.properties +++ b/sop-java-picocli/src/main/resources/msg_armor.properties @@ -4,6 +4,7 @@ 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 usage.synopsisHeading=Usage:\u0020 usage.commandListHeading = %nCommands:%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 4dbf43b..a2303e9 100644 --- a/sop-java-picocli/src/main/resources/msg_armor_de.properties +++ b/sop-java-picocli/src/main/resources/msg_armor_de.properties @@ -4,6 +4,7 @@ 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 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%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 effa2d1..0c6eff8 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 @@ -2,13 +2,19 @@ # # SPDX-License-Identifier: Apache-2.0 usage.header=Update a key's password +usage.description.0=Unlock all secret keys from STDIN using the given old passwords and emit them re-locked using the new password to STDOUT. +usage.description.1=If any (sub-) key cannot be unlocked, this operation will exit with error code 67. no-armor=ASCII armor the output -new-key-password.0=New password to lock the key(s) with. -new-key-password.1=If no new password is given, the result will be unprotected. -old-key-password.0=Old passwords to unlock the key(s) with. -old-key-password.1=You can provide more than one old password. +new-key-password.0=New password to lock the keys with. +new-key-password.1=If no new password is passed in, the keys will be emitted unlocked. +new-key-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +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...). +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 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 17b829d..014c3e7 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 @@ -2,13 +2,19 @@ # # SPDX-License-Identifier: Apache-2.0 usage.header=Ändere das Passwort eines Schlüssels +usage.description.0=Entsperre alle Schlüssel von Standard-Eingabe mithilfe der alten Passwörter und gebe sie mit dem neuen Passwort gesperrt auf Standard-Ausgabe aus. +usage.description.1=Falls einer oder mehrere (Unter-)Schlüssel nicht entsperrt werden können, gibt diese Operation den Fehlercode 67 aus. no-armor=Schütze Ausgabe mit ASCII Armor -new-key-password.0=Neues Passwort zur Sicherung des Schlüssels. -new-key-password.1=Falls kein neues Passwort angegeben wird, wird der Schlüssel ungesichert ausgegeben. -old-key-password.0=Altes Passwort zum Entsichern des Schlüssels. -old-key-password.1=Es kann mehr als ein altes Passwort angegeben werden. +new-key-password.0=Neues Passwort zur Sperrung der Schlüssel. +new-key-password.1=Falls kein neues Passwort angegeben wird, werden die Schlüssel entsperrt ausgegeben. +new-key-password.2=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +old-key-password.0=Alte Passwörter zum Entsperren der Schlüssel. +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...). +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 diff --git a/sop-java-picocli/src/main/resources/msg_dearmor.properties b/sop-java-picocli/src/main/resources/msg_dearmor.properties index 734c61a..b088de1 100644 --- a/sop-java-picocli/src/main/resources/msg_dearmor.properties +++ b/sop-java-picocli/src/main/resources/msg_dearmor.properties @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Remove ASCII Armor from standard input +stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 usage.commandListHeading = %nCommands:%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 b601e43..362ccef 100644 --- a/sop-java-picocli/src/main/resources/msg_dearmor_de.properties +++ b/sop-java-picocli/src/main/resources/msg_dearmor_de.properties @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Entferne ASCII Armor von Standard-Eingabe +stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n diff --git a/sop-java-picocli/src/main/resources/msg_decrypt.properties b/sop-java-picocli/src/main/resources/msg_decrypt.properties index c68a1b8..75adca6 100644 --- a/sop-java-picocli/src/main/resources/msg_decrypt.properties +++ b/sop-java-picocli/src/main/resources/msg_decrypt.properties @@ -11,18 +11,20 @@ with-password.1=Enables decryption based on any "SKESK" packets in the "CIPHERTE with-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). verify-out=Emits signature verification status to the designated output verify-with=Certificates for signature verification -not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) -not-before.1=Reject signatures with a creation date not in range. -not-before.2=Defaults to beginning of time ('-'). -not-after.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) -not-after.1=Reject signatures with a creation date not in range. -not-after.2=Defaults to current system time ('now'). -not-after.3=Accepts special value '-' for end of time. +verify-not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) +verify-not-before.1=Reject signatures with a creation date not in range. +verify-not-before.2=Defaults to beginning of time ('-'). +verify-not-after.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) +verify-not-after.1=Reject signatures with a creation date not in range. +verify-not-after.2=Defaults to current system time ('now'). +verify-not-after.3=Accepts special value '-' for end of time. 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 +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 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 249e39c..0aefcee 100644 --- a/sop-java-picocli/src/main/resources/msg_decrypt_de.properties +++ b/sop-java-picocli/src/main/resources/msg_decrypt_de.properties @@ -11,18 +11,20 @@ with-password.1=Erm with-password.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). verify-out=Schreibe Status der Signaturprüfung in angegebene Ausgabe verify-with=Zertifikate zur Signaturprüfung -not-before.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) -not-before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. -not-before.2=Standardmäßig: Anbeginn der Zeit ('-'). -not-after.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) -not-after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. -not-after.2=Standardmäßig: Aktueller Zeitunkt ('now'). -not-after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten. +verify-not-before.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) +verify-not-before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. +verify-not-before.2=Standardmäßig: Anbeginn der Zeit ('-'). +verify-not-after.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) +verify-not-after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. +verify-not-after.2=Standardmäßig: Aktueller Zeitunkt ('now'). +verify-not-after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten. with-key-password.0=Passwort zum Entsperren der privaten Schlüssel with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). KEY[0..*]=Private Schlüssel zum Entschlüsseln der 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 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 f21b79a..e5ea4e7 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-sign.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-sign.properties @@ -11,7 +11,9 @@ 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 +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 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 7c39ebd..2992c02 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,7 +11,9 @@ 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 +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 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 ecc75b3..27ba2ad 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-verify.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-verify.properties @@ -10,9 +10,11 @@ not-after.1=Reject signatures with a creation date not in range. not-after.2=Defaults to current system time ("now"). not-after.3=Accepts special value "-" for end of time. SIGNATURE[0]=Detached signature -CERT[0..*]=Public key certificates for signature verification +CERT[1..*]=Public key certificates for signature verification +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 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 e13ccba..76adc60 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 @@ -10,9 +10,11 @@ not-after.1=Lehne Signaturen mit Erstellungsdatum au not-after.2=Standardmäßig: Aktueller Zeitunkt ('now'). not-after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten. SIGNATURE[0]=Abgetrennte Signatur -CERT[0..*]=Zertifikate (öffentliche Schlüssel) zur Signaturprüfung +CERT[1..*]=Zertifikate (öffentliche Schlüssel) zur Signaturprüfung +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 diff --git a/sop-java-picocli/src/main/resources/msg_encrypt.properties b/sop-java-picocli/src/main/resources/msg_encrypt.properties index 7c3b218..c0f7f7d 100644 --- a/sop-java-picocli/src/main/resources/msg_encrypt.properties +++ b/sop-java-picocli/src/main/resources/msg_encrypt.properties @@ -12,7 +12,9 @@ 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 +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 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 c2ae6ef..6a3055c 100644 --- a/sop-java-picocli/src/main/resources/msg_encrypt_de.properties +++ b/sop-java-picocli/src/main/resources/msg_encrypt_de.properties @@ -12,7 +12,9 @@ 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 +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 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 76ee718..d661d2c 100644 --- a/sop-java-picocli/src/main/resources/msg_extract-cert.properties +++ b/sop-java-picocli/src/main/resources/msg_extract-cert.properties @@ -4,6 +4,7 @@ usage.header=Extract a public key certificate from a secret key from standard input no-armor=ASCII armor the output +stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 usage.commandListHeading = %nCommands:%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 ce16f7c..c536b93 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 @@ -4,6 +4,7 @@ usage.header=Extrahiere Zertifikat (öffentlichen Schlüssel) aus privatem Schlüssel von Standard-Eingabe no-armor=Schütze Ausgabe mit ASCII Armor +stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%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 e38bb65..60ff4a4 100644 --- a/sop-java-picocli/src/main/resources/msg_generate-key.properties +++ b/sop-java-picocli/src/main/resources/msg_generate-key.properties @@ -5,11 +5,13 @@ usage.header=Generate a secret key no-armor=ASCII armor the output USERID[0..*]=User-ID, e.g. "Alice " profile=Profile identifier to switch between profiles -signing-only=Generate a key that is not capable of encryption +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...). +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 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 c5f98e0..6a0ce13 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 @@ -5,11 +5,13 @@ usage.header=Generiere einen privaten Schl no-armor=Schütze Ausgabe mit ASCII Armor USERID[0..*]=Nutzer-ID, z.B.. "Alice " profile=Profil-Identifikator um zwischen Profilen zu wechseln -signing-only=Generiere einen Schlüssel, der nicht Verschlüsseln kann +signing-only=Generiere einen Schlüssel, der nur zum Signieren genutzt werden kann with-key-password.0=Passwort zum Schutz des privaten Schlüssels with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +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 diff --git a/sop-java-picocli/src/main/resources/msg_help.properties b/sop-java-picocli/src/main/resources/msg_help.properties index 1146a3e..797cc79 100644 --- a/sop-java-picocli/src/main/resources/msg_help.properties +++ b/sop-java-picocli/src/main/resources/msg_help.properties @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 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 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 9262114..beea45c 100644 --- a/sop-java-picocli/src/main/resources/msg_help_de.properties +++ b/sop-java-picocli/src/main/resources/msg_help_de.properties @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Zeige Nutzungshilfen für den angegebenen Unterbefehl an +stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%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 44f530d..c100c51 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-detach.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-detach.properties @@ -5,6 +5,7 @@ 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 +stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 usage.commandListHeading = %nCommands:%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 5177a2f..e59aa34 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,6 +5,7 @@ usage.header=Trenne Signaturen von Klartext-signierter Nachricht no-armor=Schütze Ausgabe mit ASCII Armor signatures-out=Schreibe abgetrennte Signaturen in Ausgabe +stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%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 888cbfb..5e7c59e 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-sign.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-sign.properties @@ -13,7 +13,9 @@ 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 +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 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 d0606d7..9d360d5 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,7 +13,9 @@ 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 +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 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 901fd07..c016e57 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-verify.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-verify.properties @@ -12,7 +12,9 @@ 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 +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 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 e5ea22a..3e340c0 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,7 +12,9 @@ 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 +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 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 71bf5aa..6d5f1a8 100644 --- a/sop-java-picocli/src/main/resources/msg_list-profiles.properties +++ b/sop-java-picocli/src/main/resources/msg_list-profiles.properties @@ -3,7 +3,10 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Emit a list of profiles supported by the identified subcommand subcommand=Subcommand for which to list profiles + +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 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 e6d8836..ac03c0d 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 @@ -2,8 +2,11 @@ # # SPDX-License-Identifier: Apache-2.0 usage.header=Gebe eine Liste von Profilen aus, welche vom angegebenen Unterbefehl unterstützt werden -subcommand=Unterbefehl für welchen Profile gelistet werden sollen +subcommand=Unterbefehl, für welchen Profile gelistet werden sollen + +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 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 d3e59cc..c7d72b3 100644 --- a/sop-java-picocli/src/main/resources/msg_revoke-key.properties +++ b/sop-java-picocli/src/main/resources/msg_revoke-key.properties @@ -1,9 +1,15 @@ # SPDX-FileCopyrightText: 2023 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.header=Generate revocation certificate for private keys +usage.header=Generate revocation certificates +usage.description=Emit revocation certificates for secret keys from STDIN to STDOUT. 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...). + +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 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 2f7e294..95db272 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 @@ -1,9 +1,15 @@ # SPDX-FileCopyrightText: 2023 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.header=Erzeuge Widerrufszertifikate für private Schlüssel +usage.header=Erzeuge Widerrufszertifikate +usage.description=Gebe Widerrufszertifikate für Schlüssel von Standard-Eingabe auf Standard-Ausgabe aus. no-armor=Schütze Ausgabe mit ASCII Armor +with-key-password.0=Passwort zum Entsperren der privaten Schlüssel +with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). + +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 diff --git a/sop-java-picocli/src/main/resources/msg_version.properties b/sop-java-picocli/src/main/resources/msg_version.properties index a4c75ca..9e1451b 100644 --- a/sop-java-picocli/src/main/resources/msg_version.properties +++ b/sop-java-picocli/src/main/resources/msg_version.properties @@ -6,6 +6,7 @@ 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 +stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 usage.commandListHeading = %nCommands:%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 573a82f..608b0c6 100644 --- a/sop-java-picocli/src/main/resources/msg_version_de.properties +++ b/sop-java-picocli/src/main/resources/msg_version_de.properties @@ -6,6 +6,7 @@ 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 +stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n From feb9efc7335cd85744a982c463b0902992c92cf5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 14:20:53 +0200 Subject: [PATCH 053/298] Reorder subcommands --- .../src/main/java/sop/cli/picocli/SopCLI.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java index e80c887..8eb18bf 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java @@ -32,22 +32,27 @@ import java.util.ResourceBundle; resourceBundle = "msg_sop", exitCodeOnInvalidInput = 69, subcommands = { - CommandLine.HelpCommand.class, - ArmorCmd.class, - DearmorCmd.class, - DecryptCmd.class, - InlineDetachCmd.class, - EncryptCmd.class, - ExtractCertCmd.class, + // Meta Subcommands + VersionCmd.class, + ListProfilesCmd.class, + // Key and Certificate Management Subcommands GenerateKeyCmd.class, + ChangeKeyPasswordCmd.class, + RevokeKeyCmd.class, + ExtractCertCmd.class, + // Messaging Subcommands SignCmd.class, VerifyCmd.class, + EncryptCmd.class, + DecryptCmd.class, + InlineDetachCmd.class, InlineSignCmd.class, InlineVerifyCmd.class, - ListProfilesCmd.class, - RevokeKeyCmd.class, - ChangeKeyPasswordCmd.class, - VersionCmd.class, + // Transport Subcommands + ArmorCmd.class, + DearmorCmd.class, + // Miscellaneous Subcommands + CommandLine.HelpCommand.class, AutoComplete.GenerateCompletion.class } ) From edb384d1572e3e38dc72280975453278ad65c0da Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 14:22:57 +0200 Subject: [PATCH 054/298] Reference exit codes by Exception --- sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java | 3 ++- .../src/main/java/sop/cli/picocli/commands/EncryptCmd.java | 2 +- .../src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java | 2 +- .../src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java | 2 +- .../src/main/java/sop/cli/picocli/commands/InlineSignCmd.java | 2 +- .../main/java/sop/cli/picocli/commands/InlineVerifyCmd.java | 2 +- .../main/java/sop/cli/picocli/commands/ListProfilesCmd.java | 2 +- .../src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java | 2 +- .../src/main/java/sop/cli/picocli/commands/SignCmd.java | 2 +- .../src/main/java/sop/cli/picocli/commands/VerifyCmd.java | 2 +- .../src/main/java/sop/cli/picocli/commands/VersionCmd.java | 3 ++- 11 files changed, 13 insertions(+), 11 deletions(-) diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java index 8eb18bf..5420dea 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java @@ -22,6 +22,7 @@ import sop.cli.picocli.commands.RevokeKeyCmd; import sop.cli.picocli.commands.SignCmd; import sop.cli.picocli.commands.VerifyCmd; import sop.cli.picocli.commands.VersionCmd; +import sop.exception.SOPGPException; import java.util.List; import java.util.Locale; @@ -30,7 +31,7 @@ import java.util.ResourceBundle; @CommandLine.Command( name = "sop", resourceBundle = "msg_sop", - exitCodeOnInvalidInput = 69, + exitCodeOnInvalidInput = SOPGPException.UnsupportedSubcommand.EXIT_CODE, subcommands = { // Meta Subcommands VersionCmd.class, diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java index 079f9d8..efda26f 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java @@ -18,7 +18,7 @@ import java.util.List; @CommandLine.Command(name = "encrypt", resourceBundle = "msg_encrypt", - exitCodeOnInvalidInput = 37) + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) public class EncryptCmd extends AbstractSopCmd { @CommandLine.Option(names = "--no-armor", diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java index 28a74b0..64a7a84 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java @@ -14,7 +14,7 @@ import sop.operation.ExtractCert; @CommandLine.Command(name = "extract-cert", resourceBundle = "msg_extract-cert", - exitCodeOnInvalidInput = 37) + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) public class ExtractCertCmd extends AbstractSopCmd { @CommandLine.Option(names = "--no-armor", diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java index a41f086..eea992e 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java @@ -16,7 +16,7 @@ import java.util.List; @CommandLine.Command(name = "generate-key", resourceBundle = "msg_generate-key", - exitCodeOnInvalidInput = 37) + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) public class GenerateKeyCmd extends AbstractSopCmd { @CommandLine.Option(names = "--no-armor", diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java index 773e6af..1865bcf 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java @@ -18,7 +18,7 @@ import java.util.List; @CommandLine.Command(name = "inline-sign", resourceBundle = "msg_inline-sign", - exitCodeOnInvalidInput = 37) + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) public class InlineSignCmd extends AbstractSopCmd { @CommandLine.Option(names = "--no-armor", diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java index c3b73c5..c413c85 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java @@ -20,7 +20,7 @@ import java.util.List; @CommandLine.Command(name = "inline-verify", resourceBundle = "msg_inline-verify", - exitCodeOnInvalidInput = 37) + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) public class InlineVerifyCmd extends AbstractSopCmd { @CommandLine.Parameters(arity = "0..*", diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java index 46464f8..53ec024 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java @@ -13,7 +13,7 @@ import sop.operation.ListProfiles; @CommandLine.Command(name = "list-profiles", resourceBundle = "msg_list-profiles", - exitCodeOnInvalidInput = 37) + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) public class ListProfilesCmd extends AbstractSopCmd { @CommandLine.Parameters(paramLabel = "COMMAND", arity = "1", descriptionKey = "subcommand") diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java index 28d1890..45f22fa 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java @@ -14,7 +14,7 @@ import java.io.IOException; @CommandLine.Command(name = "revoke-key", resourceBundle = "msg_revoke-key", - exitCodeOnInvalidInput = 37) + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) public class RevokeKeyCmd extends AbstractSopCmd { @CommandLine.Option(names = "--no-armor", diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java index 521262b..cad9d6e 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java @@ -21,7 +21,7 @@ import java.util.List; @CommandLine.Command(name = "sign", resourceBundle = "msg_detached-sign", - exitCodeOnInvalidInput = 37) + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) public class SignCmd extends AbstractSopCmd { @CommandLine.Option(names = "--no-armor", diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java index 1bcd0a3..d76bb37 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java @@ -18,7 +18,7 @@ import java.util.List; @CommandLine.Command(name = "verify", resourceBundle = "msg_detached-verify", - exitCodeOnInvalidInput = 37) + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) public class VerifyCmd extends AbstractSopCmd { @CommandLine.Parameters(index = "0", diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java index c11d050..6ccb8f7 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java @@ -7,10 +7,11 @@ package sop.cli.picocli.commands; import picocli.CommandLine; import sop.cli.picocli.Print; import sop.cli.picocli.SopCLI; +import sop.exception.SOPGPException; import sop.operation.Version; @CommandLine.Command(name = "version", resourceBundle = "msg_version", - exitCodeOnInvalidInput = 37) + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) public class VersionCmd extends AbstractSopCmd { @CommandLine.ArgGroup() From cd2c62ce2bce6f57274364cde662d76bfd1863d7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 14:30:23 +0200 Subject: [PATCH 055/298] Improve description of change-key-password --- .../src/main/resources/msg_change-key-password.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0c6eff8..3de3608 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 @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2023 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.header=Update a key's password +usage.header=Update the password of a key usage.description.0=Unlock all secret keys from STDIN using the given old passwords and emit them re-locked using the new password to STDOUT. usage.description.1=If any (sub-) key cannot be unlocked, this operation will exit with error code 67. no-armor=ASCII armor the output From 9ad59abb2a62741ddba6338e5ee895917226616d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 14:30:45 +0200 Subject: [PATCH 056/298] Improve description of extract-cert --- .../src/main/resources/msg_extract-cert.properties | 4 +++- .../src/main/resources/msg_extract-cert_de.properties | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) 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 d661d2c..82cac0f 100644 --- a/sop-java-picocli/src/main/resources/msg_extract-cert.properties +++ b/sop-java-picocli/src/main/resources/msg_extract-cert.properties @@ -1,11 +1,13 @@ # SPDX-FileCopyrightText: 2022 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.header=Extract a public key certificate from a secret key from standard input +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 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 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 c536b93..0946cfc 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 @@ -1,11 +1,13 @@ # SPDX-FileCopyrightText: 2022 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.header=Extrahiere Zertifikat (öffentlichen Schlüssel) aus privatem Schlüssel von Standard-Eingabe +usage.header=Extrahiere Zertifikat (öffentlichen Schlüssel) aus privatem Schlüssel +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 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 From 530c44ad16aaef50f70b737615786c74396becc9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 14:57:51 +0200 Subject: [PATCH 057/298] Typos and improvements --- sop-java-picocli/src/main/resources/msg_decrypt.properties | 2 +- .../src/main/resources/msg_decrypt_de.properties | 6 +++--- .../src/main/resources/msg_detached-sign.properties | 2 +- .../src/main/resources/msg_detached-sign_de.properties | 2 +- .../src/main/resources/msg_detached-verify.properties | 4 +++- .../src/main/resources/msg_detached-verify_de.properties | 6 ++++-- .../src/main/resources/msg_inline-sign.properties | 2 +- .../src/main/resources/msg_inline-sign_de.properties | 2 +- .../src/main/resources/msg_inline-verify.properties | 2 +- .../src/main/resources/msg_inline-verify_de.properties | 4 ++-- 10 files changed, 18 insertions(+), 14 deletions(-) diff --git a/sop-java-picocli/src/main/resources/msg_decrypt.properties b/sop-java-picocli/src/main/resources/msg_decrypt.properties index 75adca6..5903ded 100644 --- a/sop-java-picocli/src/main/resources/msg_decrypt.properties +++ b/sop-java-picocli/src/main/resources/msg_decrypt.properties @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2022 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.header=Decrypt a message from standard input +usage.header=Decrypt a message session-key-out=Can be used to learn the session key on successful decryption with-session-key.0=Symmetric message key (session key). with-session-key.1=Enables decryption of the "CIPHERTEXT" using the session key directly against the "SEIPD" packet. 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 0aefcee..ba40897 100644 --- a/sop-java-picocli/src/main/resources/msg_decrypt_de.properties +++ b/sop-java-picocli/src/main/resources/msg_decrypt_de.properties @@ -1,10 +1,10 @@ # SPDX-FileCopyrightText: 2022 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.header=Entschlüssle eine Nachricht von Standard-Eingabe +usage.header=Entschlüssle eine Nachricht session-key-out=Extrahiere den Nachrichtenschlüssel nach erfolgreicher Entschlüsselung with-session-key.0=Symmetrischer Nachrichtenschlüssel (Sitzungsschlüssel). -with-session-key.1=Ermöglicht direkte Entschlüsselung des im "CIPHERTEXT" enhaltenen "SEIPD" Paketes mithilfe des Nachrichtenschlüssels. +with-session-key.1=Ermöglicht direkte Entschlüsselung des im "CIPHERTEXT" enthaltenen "SEIPD" Paketes mithilfe des Nachrichtenschlüssels. with-session-key.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). with-password.0=Symmetrisches Passwort zur Entschlüsselung der Nachricht. with-password.1=Ermöglicht Entschlüsselung basierend auf im "CIPHERTEXT" enthaltenen "SKESK" Paketen. @@ -16,7 +16,7 @@ verify-not-before.1=Lehne Signaturen mit Erstellungsdatum au verify-not-before.2=Standardmäßig: Anbeginn der Zeit ('-'). verify-not-after.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) verify-not-after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. -verify-not-after.2=Standardmäßig: Aktueller Zeitunkt ('now'). +verify-not-after.2=Standardmäßig: Aktueller Zeitpunkt ('now'). verify-not-after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten. with-key-password.0=Passwort zum Entsperren der privaten Schlüssel with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). 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 e5ea4e7..83359a6 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-sign.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-sign.properties @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2022 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.header=Create a detached signature on the data from standard input +usage.header=Create a detached message signature no-armor=ASCII armor the output as.0=Specify the output format of the signed message. as.1=Defaults to 'binary'. 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 2992c02..b943da5 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 @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2022 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.header=Erstelle abgetrennte Signatur über Nachricht von Standard-Eingabe +usage.header=Erstelle abgetrennte Nachrichten-Signatur no-armor=Schütze Ausgabe mit ASCII Armor as.0=Bestimme Signaturformat der Nachricht. as.1=Standardmäßig: 'binary'. 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 27ba2ad..ee1a468 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-verify.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-verify.properties @@ -1,7 +1,8 @@ # SPDX-FileCopyrightText: 2022 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.header=Verify a detached signature over the data from standard input +usage.header=Verify a detached signature +usage.description=Verify a detached signature over some data from STDIN. not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) not-before.1=Reject signatures with a creation date not in range. not-before.2=Defaults to beginning of time ("-"). @@ -14,6 +15,7 @@ CERT[1..*]=Public key certificates for signature verification 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 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 76adc60..332bff6 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 @@ -1,19 +1,21 @@ # SPDX-FileCopyrightText: 2022 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.header=Prüfe eine abgetrennte Signatur über eine Nachricht von Standard-Eingabe +usage.header=Prüfe eine abgetrennte Nachrichten-Signatur +usage.description=Prüfe eine abgetrennte Signatur über eine Nachricht von Standard-Eingabe. not-before.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) not-before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. not-before.2=Standardmäßig: Anbeginn der Zeit ('-'). not-after.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) not-after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. -not-after.2=Standardmäßig: Aktueller Zeitunkt ('now'). +not-after.2=Standardmäßig: Aktueller Zeitpunkt ('now'). not-after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten. SIGNATURE[0]=Abgetrennte Signatur CERT[1..*]=Zertifikate (öffentliche Schlüssel) zur Signaturprüfung 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 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 5e7c59e..f5143bb 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-sign.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-sign.properties @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2022 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.header=Create an inline-signed message from data on standard input +usage.header=Create an inline-signed message no-armor=ASCII armor the output as.0=Specify the signature format of the signed message. as.1='text' and 'binary' will produce inline-signed messages. 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 9d360d5..b09b7e4 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 @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2022 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.header=Signiere eine Nachricht von Standard-Eingabe mit eingebetteten Signaturen +usage.header=Signiere eine Nachricht mit eingebetteten Signaturen no-armor=Schütze Ausgabe mit ASCII Armor as.0=Bestimme Signaturformat der Nachricht. as.1='text' und 'binary' resultieren in eingebettete Signaturen. 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 c016e57..dfa94d7 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-verify.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-verify.properties @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2022 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.header=Verify inline-signed data from standard input +usage.header=Verify an inline-signed message not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) not-before.1=Reject signatures with a creation date not in range. not-before.2=Defaults to beginning of time ("-"). 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 3e340c0..a9a5722 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 @@ -1,13 +1,13 @@ # SPDX-FileCopyrightText: 2022 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.header=Prüfe eingebettete Signaturen einer Nachricht von Standard-Eingabe +usage.header=Prüfe eingebettete Signaturen einer Nachricht not-before.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) not-before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. not-before.2=Standardmäßig: Anbeginn der Zeit ('-'). not-after.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) not-after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. -not-after.2=Standardmäßig: Aktueller Zeitunkt ('now'). +not-after.2=Standardmäßig: Aktueller Zeitpunkt ('now'). not-after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten. verifications-out=Schreibe Status der Signaturprüfung in angegebene Ausgabe CERT[0..*]=Zertifikate (öffentlich Schlüssel) zur Signaturprüfung From be351616b68cf3759418156a233293e5384e217e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 15:08:33 +0200 Subject: [PATCH 058/298] Add some more RevokeKey tests --- .../testsuite/operation/RevokeKeyTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java index 920f877..10472ca 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java @@ -10,6 +10,8 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import sop.SOP; import sop.exception.SOPGPException; +import sop.testsuite.JUtils; +import sop.testsuite.TestData; import sop.util.UTF8Util; import java.io.IOException; @@ -18,6 +20,7 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") public class RevokeKeyTest extends AbstractSOPTest { @@ -32,9 +35,29 @@ public class RevokeKeyTest extends AbstractSOPTest { byte[] secretKey = sop.generateKey().userId("Alice ").generate().getBytes(); byte[] revocation = sop.revokeKey().keys(secretKey).getBytes(); + assertTrue(JUtils.arrayStartsWith(revocation, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); assertFalse(Arrays.equals(secretKey, revocation)); } + @ParameterizedTest + @MethodSource("provideInstances") + public void revokeUnprotectedKeyUnarmored(SOP sop) throws IOException { + byte[] secretKey = sop.generateKey().userId("Alice ").noArmor().generate().getBytes(); + byte[] revocation = sop.revokeKey().noArmor().keys(secretKey).getBytes(); + + assertFalse(JUtils.arrayStartsWith(revocation, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); + assertFalse(Arrays.equals(secretKey, revocation)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void revokeCertificateFails(SOP sop) throws IOException { + byte[] secretKey = sop.generateKey().generate().getBytes(); + byte[] certificate = sop.extractCert().key(secretKey).getBytes(); + + assertThrows(SOPGPException.BadData.class, () -> sop.revokeKey().keys(certificate).getBytes()); + } + @ParameterizedTest @MethodSource("provideInstances") public void revokeProtectedKey(SOP sop) throws IOException { From 308c4b452f9639bb2cfaa36620c5e40eab7959fa Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 15:36:39 +0200 Subject: [PATCH 059/298] Add test for signature verification with hard-revoked cert --- .../testsuite/operation/RevokeKeyTest.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java index 10472ca..6595133 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java @@ -9,13 +9,17 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import sop.SOP; +import sop.Verification; import sop.exception.SOPGPException; import sop.testsuite.JUtils; import sop.testsuite.TestData; +import sop.testsuite.assertions.VerificationListAssert; import sop.util.UTF8Util; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.List; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -97,4 +101,23 @@ public class RevokeKeyTest extends AbstractSOPTest { assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.revokeKey().withKeyPassword(wrongPassword).keys(secretKey).getBytes()); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void revokeKeyIsNowHardRevoked(SOP sop) throws IOException { + byte[] key = sop.generateKey().generate().getBytes(); + byte[] cert = sop.extractCert().key(key).getBytes(); + + // Sign a message with the key + byte[] msg = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + byte[] signedMsg = sop.inlineSign().key(key).data(msg).getBytes(); + + // Verifying the message with the valid cert works + List result = sop.inlineVerify().cert(cert).data(signedMsg).toByteArrayAndResult().getResult(); + VerificationListAssert.assertThatVerificationList(result).hasSingleItem(); + + // Now hard revoke the key and re-check signature, expecting no valid certification + byte[] revokedCert = sop.revokeKey().keys(key).getBytes(); + assertThrows(SOPGPException.NoSignature.class, () -> sop.inlineVerify().cert(revokedCert).data(signedMsg).toByteArrayAndResult()); + } } From f2073dcbf4d1bdf1535a09c30e719c47504d7a4f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 15:41:47 +0200 Subject: [PATCH 060/298] SOP-Java 7.0.0 --- CHANGELOG.md | 5 ++--- version.gradle | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7c3476..3ca12a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,13 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 7.0.0-SNAPSHOT +## 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 - Add support for new `change-key-password` subcommand - Add support for new `--signing-only` option of `generate-key` subcommand - -## 6.1.1-SNAPSHOT - Add `dearmor.data(String)` utility method +- Fix typos in, and improve i18n of CLI help pages ## 6.1.0 - `listProfiles()`: Add shortcut methods `generateKey()` and `encrypt()` diff --git a/version.gradle b/version.gradle index 7614d8f..ed3e547 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '7.0.0' - isSnapshot = true + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From cf1d39643d3c0d7d56379eba9a1f5c6368d0bc12 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 15:44:05 +0200 Subject: [PATCH 061/298] SOP-Java 7.0.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index ed3e547..d8ddfd6 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '7.0.0' - isSnapshot = false + shortVersion = '7.0.1' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 4bd46579068977ef12efc8ac8cc65ed73c8be747 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 11:24:27 +0100 Subject: [PATCH 062/298] Add kotlin plugin --- build.gradle | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/build.gradle b/build.gradle index 56730fd..577c2aa 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,8 @@ buildscript { plugins { id 'ru.vyarus.animalsniffer' version '1.5.3' + id 'org.jetbrains.kotlin.jvm' version "1.8.10" + id 'com.diffplug.spotless' version '6.22.0' apply false } apply from: 'version.gradle' @@ -29,6 +31,8 @@ allprojects { apply plugin: 'eclipse' apply plugin: 'jacoco' apply plugin: 'checkstyle' + apply plugin: 'kotlin' + apply plugin: 'com.diffplug.spotless' // For non-cli modules enable android api compatibility check if (it.name.equals('sop-java')) { @@ -53,6 +57,12 @@ allprojects { toolVersion = '8.18' } + spotless { + kotlin { + ktfmt().dropboxStyle() + } + } + group 'org.pgpainless' description = "Stateless OpenPGP Protocol API for Java" version = shortVersion @@ -69,6 +79,13 @@ allprojects { reproducibleFileOrder = true } + // Compatibility of default implementations in kotlin interfaces with Java implementations. + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + freeCompilerArgs += ["-Xjvm-default=all-compatibility"] + } + } + project.ext { rootConfigDir = new File(rootDir, 'config') gitCommit = getGitCommit() From 0f5270c28da4f63c364a9f1d2561355a0f2f6fa9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 11:24:36 +0100 Subject: [PATCH 063/298] Kotlin conversion: ByteArrayAndResult --- .../src/main/java/sop/ByteArrayAndResult.java | 50 ------------------- .../src/main/kotlin/sop/ByteArrayAndResult.kt | 43 ++++++++++++++++ 2 files changed, 43 insertions(+), 50 deletions(-) delete mode 100644 sop-java/src/main/java/sop/ByteArrayAndResult.java create mode 100644 sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt diff --git a/sop-java/src/main/java/sop/ByteArrayAndResult.java b/sop-java/src/main/java/sop/ByteArrayAndResult.java deleted file mode 100644 index fd2b39a..0000000 --- a/sop-java/src/main/java/sop/ByteArrayAndResult.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; - -/** - * Tuple of a byte array and associated result object. - * @param type of result - */ -public class ByteArrayAndResult { - - private final byte[] bytes; - private final T result; - - public ByteArrayAndResult(byte[] bytes, T result) { - this.bytes = bytes; - this.result = result; - } - - /** - * Return the byte array part. - * - * @return bytes - */ - public byte[] getBytes() { - return bytes; - } - - /** - * Return the result part. - * - * @return result - */ - public T getResult() { - return result; - } - - /** - * Return the byte array part as an {@link InputStream}. - * - * @return input stream - */ - public InputStream getInputStream() { - return new ByteArrayInputStream(getBytes()); - } -} diff --git a/sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt b/sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt new file mode 100644 index 0000000..021a2d9 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.io.InputStream + +/** + * Tuple of a [ByteArray] and associated result object. + * + * @param bytes byte array + * @param result result object + * @param type of result + */ +data class ByteArrayAndResult(val bytes: ByteArray, val result: T) { + + /** + * [InputStream] returning the contents of [bytes]. + * + * @return input stream + */ + val inputStream: InputStream + get() = bytes.inputStream() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ByteArrayAndResult<*> + + if (!bytes.contentEquals(other.bytes)) return false + if (result != other.result) return false + + return true + } + + override fun hashCode(): Int { + var hashCode = bytes.contentHashCode() + hashCode = 31 * hashCode + (result?.hashCode() ?: 0) + return hashCode + } +} From 567571cf6cbc657e6f4713dea38bda7f34eb63ac Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 11:33:15 +0100 Subject: [PATCH 064/298] Kotlin conversion: SessionKey --- sop-java/src/main/java/sop/SessionKey.java | 80 ---------------------- sop-java/src/main/kotlin/sop/SessionKey.kt | 48 +++++++++++++ 2 files changed, 48 insertions(+), 80 deletions(-) delete mode 100644 sop-java/src/main/java/sop/SessionKey.java create mode 100644 sop-java/src/main/kotlin/sop/SessionKey.kt diff --git a/sop-java/src/main/java/sop/SessionKey.java b/sop-java/src/main/java/sop/SessionKey.java deleted file mode 100644 index 722666d..0000000 --- a/sop-java/src/main/java/sop/SessionKey.java +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.util.Arrays; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import sop.util.HexUtil; - -public class SessionKey { - - private static final Pattern PATTERN = Pattern.compile("^(\\d):([0-9A-F]+)$"); - - private final byte algorithm; - private final byte[] sessionKey; - - public SessionKey(byte algorithm, byte[] sessionKey) { - this.algorithm = algorithm; - this.sessionKey = sessionKey; - } - - /** - * Return the symmetric algorithm octet. - * - * @return algorithm id - */ - public byte getAlgorithm() { - return algorithm; - } - - /** - * Return the session key. - * - * @return session key - */ - public byte[] getKey() { - return sessionKey; - } - - @Override - public int hashCode() { - return getAlgorithm() * 17 + Arrays.hashCode(getKey()); - } - - @Override - public boolean equals(Object other) { - if (other == null) { - return false; - } - if (this == other) { - return true; - } - if (!(other instanceof SessionKey)) { - return false; - } - - SessionKey otherKey = (SessionKey) other; - return getAlgorithm() == otherKey.getAlgorithm() && Arrays.equals(getKey(), otherKey.getKey()); - } - - public static SessionKey fromString(String string) { - string = string.trim().toUpperCase().replace("\n", ""); - Matcher matcher = PATTERN.matcher(string); - if (!matcher.matches()) { - throw new IllegalArgumentException("Provided session key does not match expected format."); - } - byte algorithm = Byte.parseByte(matcher.group(1)); - String key = matcher.group(2); - - return new SessionKey(algorithm, HexUtil.hexToBytes(key)); - } - - @Override - public String toString() { - return Integer.toString(getAlgorithm()) + ':' + HexUtil.bytesToHex(sessionKey); - } -} diff --git a/sop-java/src/main/kotlin/sop/SessionKey.kt b/sop-java/src/main/kotlin/sop/SessionKey.kt new file mode 100644 index 0000000..af230f3 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/SessionKey.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import sop.util.HexUtil + +/** + * Class representing a symmetric session key. + * + * @param algorithm symmetric key algorithm ID + * @param key [ByteArray] containing the session key + */ +data class SessionKey(val algorithm: Byte, val key: ByteArray) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SessionKey + + if (algorithm != other.algorithm) return false + if (!key.contentEquals(other.key)) return false + + return true + } + + override fun hashCode(): Int { + var hashCode = algorithm.toInt() + hashCode = 31 * hashCode + key.contentHashCode() + return hashCode + } + + override fun toString(): String = "$algorithm:${HexUtil.bytesToHex(key)}" + + companion object { + + @JvmStatic private val PATTERN = "^(\\d):([0-9A-F]+)$".toPattern() + + @JvmStatic + fun fromString(string: String): SessionKey { + val matcher = PATTERN.matcher(string.trim().uppercase().replace("\n", "")) + require(matcher.matches()) { "Provided session key does not match expected format." } + return SessionKey(matcher.group(1).toByte(), HexUtil.hexToBytes(matcher.group(2))) + } + } +} From 6c5c4b3d981aa62f28dfde36a2e56821d279620f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 12:25:54 +0100 Subject: [PATCH 065/298] Kotlin conversion: Verification --- sop-java/src/main/java/sop/Verification.java | 222 ------------------- sop-java/src/main/kotlin/sop/Verification.kt | 76 +++++++ 2 files changed, 76 insertions(+), 222 deletions(-) delete mode 100644 sop-java/src/main/java/sop/Verification.java create mode 100644 sop-java/src/main/kotlin/sop/Verification.kt diff --git a/sop-java/src/main/java/sop/Verification.java b/sop-java/src/main/java/sop/Verification.java deleted file mode 100644 index 140e23b..0000000 --- a/sop-java/src/main/java/sop/Verification.java +++ /dev/null @@ -1,222 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import sop.enums.SignatureMode; -import sop.util.Optional; -import sop.util.UTCUtil; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.text.ParseException; -import java.util.Date; - -/** - * Class bundling information about a verified signature. - */ -public class Verification { - - private final Date creationTime; - private final String signingKeyFingerprint; - private final String signingCertFingerprint; - private final Optional signatureMode; - private final Optional description; - - private static final String MODE = "mode:"; - - /** - * Create a new {@link Verification} without mode and description. - * - * @param creationTime signature creation time - * @param signingKeyFingerprint fingerprint of the signing (sub-) key - * @param signingCertFingerprint fingerprint of the certificate - */ - public Verification(@Nonnull Date creationTime, - @Nonnull String signingKeyFingerprint, - @Nonnull String signingCertFingerprint) { - this(creationTime, signingKeyFingerprint, signingCertFingerprint, Optional.ofEmpty(), Optional.ofEmpty()); - } - - /** - * Create a new {@link Verification}. - * - * @param creationTime signature creation time - * @param signingKeyFingerprint fingerprint of the signing (sub-) key - * @param signingCertFingerprint fingerprint of the certificate - * @param signatureMode signature mode (optional, may be
null
) - * @param description free-form description, e.g.
certificate from dkg.asc
(optional, may be
null
) - */ - public Verification(@Nonnull Date creationTime, - @Nonnull String signingKeyFingerprint, - @Nonnull String signingCertFingerprint, - @Nullable SignatureMode signatureMode, - @Nullable String description) { - this( - creationTime, - signingKeyFingerprint, - signingCertFingerprint, - Optional.ofNullable(signatureMode), - Optional.ofNullable(nullSafeTrim(description)) - ); - } - - private Verification(@Nonnull Date creationTime, - @Nonnull String signingKeyFingerprint, - @Nonnull String signingCertFingerprint, - @Nonnull Optional signatureMode, - @Nonnull Optional description) { - this.creationTime = creationTime; - this.signingKeyFingerprint = signingKeyFingerprint; - this.signingCertFingerprint = signingCertFingerprint; - this.signatureMode = signatureMode; - this.description = description; - } - - private static String nullSafeTrim(@Nullable String string) { - if (string == null) { - return null; - } - return string.trim(); - } - - @Nonnull - public static Verification fromString(@Nonnull String toString) { - String[] split = toString.trim().split(" "); - if (split.length < 3) { - throw new IllegalArgumentException("Verification must be of the format 'UTC-DATE OpenPGPFingerprint OpenPGPFingerprint [mode] [info]'"); - } - - if (split.length == 3) { - return new Verification( - parseUTCDate(split[0]), // timestamp - split[1], // key FP - split[2] // cert FP - ); - } - - SignatureMode mode = null; - int index = 3; - if (split[index].startsWith(MODE)) { - mode = SignatureMode.valueOf(split[3].substring(MODE.length())); - index++; - } - - StringBuilder sb = new StringBuilder(); - for (int i = index; i < split.length; i++) { - if (sb.length() != 0) { - sb.append(' '); - } - sb.append(split[i]); - } - - return new Verification( - parseUTCDate(split[0]), // timestamp - split[1], // key FP - split[2], // cert FP - mode, // signature mode - sb.length() != 0 ? sb.toString() : null // description - ); - } - - private static Date parseUTCDate(String utcFormatted) { - try { - return UTCUtil.parseUTCDate(utcFormatted); - } catch (ParseException e) { - throw new IllegalArgumentException("Malformed UTC timestamp.", e); - } - } - - /** - * Return the signatures' creation time. - * - * @return signature creation time - */ - @Nonnull - public Date getCreationTime() { - return creationTime; - } - - /** - * Return the fingerprint of the signing (sub)key. - * - * @return signing key fingerprint - */ - @Nonnull - public String getSigningKeyFingerprint() { - return signingKeyFingerprint; - } - - /** - * Return the fingerprint fo the signing certificate. - * - * @return signing certificate fingerprint - */ - @Nonnull - public String getSigningCertFingerprint() { - return signingCertFingerprint; - } - - /** - * Return the mode of the signature. - * Optional, may return
null
. - * - * @return signature mode - */ - @Nonnull - public Optional getSignatureMode() { - return signatureMode; - } - - /** - * Return an optional description. - * Optional, may return
null
. - * - * @return description - */ - @Nonnull - public Optional getDescription() { - return description; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder() - .append(UTCUtil.formatUTCDate(getCreationTime())) - .append(' ') - .append(getSigningKeyFingerprint()) - .append(' ') - .append(getSigningCertFingerprint()); - - if (signatureMode.isPresent()) { - sb.append(' ').append(MODE).append(signatureMode.get()); - } - - if (description.isPresent()) { - sb.append(' ').append(description.get()); - } - - return sb.toString(); - } - - @Override - public int hashCode() { - return toString().hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (this == obj) { - return true; - } - if (!(obj instanceof Verification)) { - return false; - } - Verification other = (Verification) obj; - return toString().equals(other.toString()); - } -} diff --git a/sop-java/src/main/kotlin/sop/Verification.kt b/sop-java/src/main/kotlin/sop/Verification.kt new file mode 100644 index 0000000..20401a9 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/Verification.kt @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.text.ParseException +import java.util.Date +import sop.enums.SignatureMode +import sop.util.Optional +import sop.util.UTCUtil + +data class Verification( + val creationTime: Date, + val signingKeyFingerprint: String, + val signingCertFingerprint: String, + val signatureMode: Optional, + val description: Optional +) { + @JvmOverloads + constructor( + creationTime: Date, + signingKeyFingerprint: String, + signingCertFingerprint: String, + signatureMode: SignatureMode? = null, + description: String? = null + ) : this( + creationTime, + signingKeyFingerprint, + signingCertFingerprint, + Optional.ofNullable(signatureMode), + Optional.ofNullable(description?.trim())) + + override fun toString(): String = + "${UTCUtil.formatUTCDate(creationTime)} $signingKeyFingerprint $signingCertFingerprint" + + (if (signatureMode.isPresent) " mode:${signatureMode.get()}" else "") + + (if (description.isPresent) " ${description.get()}" else "") + + companion object { + @JvmStatic + fun fromString(string: String): Verification { + val split = string.trim().split(" ") + require(split.size >= 3) { + "Verification must be of the format 'UTC-DATE OpenPGPFingerprint OpenPGPFingerprint [mode] [info]'." + } + if (split.size == 3) { + return Verification(parseUTCDate(split[0]), split[1], split[2]) + } + + var index = 3 + val mode = + if (split[3].startsWith("mode:")) { + index += 1 + SignatureMode.valueOf(split[3].substring("mode:".length)) + } else null + + val description = split.subList(index, split.size).joinToString(" ").ifBlank { null } + + return Verification( + parseUTCDate(split[0]), + split[1], + split[2], + Optional.ofNullable(mode), + Optional.ofNullable(description)) + } + + @JvmStatic + private fun parseUTCDate(string: String): Date { + return try { + UTCUtil.parseUTCDate(string) + } catch (e: ParseException) { + throw IllegalArgumentException("Malformed UTC timestamp.", e) + } + } + } +} From ef4b01c6bd73d74a3b3d203ec5da2a0228431f4e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 12:42:13 +0100 Subject: [PATCH 066/298] Kotlin conversion: Profile --- sop-java/src/main/java/sop/Profile.java | 143 ------------------------ sop-java/src/main/kotlin/sop/Profile.kt | 87 ++++++++++++++ 2 files changed, 87 insertions(+), 143 deletions(-) delete mode 100644 sop-java/src/main/java/sop/Profile.java create mode 100644 sop-java/src/main/kotlin/sop/Profile.kt diff --git a/sop-java/src/main/java/sop/Profile.java b/sop-java/src/main/java/sop/Profile.java deleted file mode 100644 index 4ea9e71..0000000 --- a/sop-java/src/main/java/sop/Profile.java +++ /dev/null @@ -1,143 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import sop.util.Optional; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Tuple class bundling a profile name and description. - * - * @see - * SOP Spec - Profile - */ -public class Profile { - - private final String name; - private final Optional description; - - /** - * Create a new {@link Profile} object. - * The {@link #toString()} representation MUST NOT exceed a length of 1000 bytes. - * - * @param name profile name - * @param description profile description - */ - public Profile(@Nonnull String name, @Nullable String description) { - if (name.trim().isEmpty()) { - throw new IllegalArgumentException("Name cannot be empty."); - } - if (name.contains(":")) { - throw new IllegalArgumentException("Name cannot contain ':'."); - } - if (name.contains(" ") || name.contains("\n") || name.contains("\t") || name.contains("\r")) { - throw new IllegalArgumentException("Name cannot contain whitespace characters."); - } - - this.name = name; - - if (description == null) { - this.description = Optional.ofEmpty(); - } else { - String trimmedDescription = description.trim(); - if (trimmedDescription.isEmpty()) { - this.description = Optional.ofEmpty(); - } else { - this.description = Optional.of(trimmedDescription); - } - } - - if (exceeds1000CharLineLimit(this)) { - throw new IllegalArgumentException("The line representation of a profile MUST NOT exceed 1000 bytes."); - } - } - - public Profile(String name) { - this(name, null); - } - - /** - * Parse a {@link Profile} from its string representation. - * - * @param string string representation - * @return profile - */ - public static Profile parse(String string) { - if (string.contains(": ")) { - // description after colon, e.g. "default: Use implementers recommendations." - String name = string.substring(0, string.indexOf(": ")); - String description = string.substring(string.indexOf(": ") + 2); - return new Profile(name, description.trim()); - } - - if (string.endsWith(":")) { - // empty description, e.g. "default:" - return new Profile(string.substring(0, string.length() - 1)); - } - - // no description - return new Profile(string.trim()); - } - - /** - * Return the name (also known as identifier) of the profile. - * A profile name is a UTF-8 string that has no whitespace in it. - * Similar to OpenPGP Notation names, profile names are divided into two namespaces: - * The IETF namespace and the user namespace. - * A profile name in the user namespace ends with the
@
character (0x40) followed by a DNS domain name. - * A profile name in the IETF namespace does not have an
@
character. - * A profile name in the user space is owned and controlled by the owner of the domain in the suffix. - * A profile name in the IETF namespace that begins with the string
rfc
should have semantics that hew as - * closely as possible to the referenced RFC. - * Similarly, a profile name in the IETF namespace that begins with the string
draft-
should have - * semantics that hew as closely as possible to the referenced Internet Draft. - * - * @return name - */ - @Nonnull - public String getName() { - return name; - } - - /** - * Return a free-form description of the profile. - * - * @return description - */ - @Nonnull - public Optional getDescription() { - return description; - } - - public boolean hasDescription() { - return description.isPresent(); - } - - /** - * Convert the profile into a String for displaying. - * - * @return string - */ - @Override - public String toString() { - if (getDescription().isEmpty()) { - return getName(); - } - return getName() + ": " + getDescription().get(); - } - - /** - * Test if the string representation of the profile exceeds the limit of 1000 bytes length. - * @param profile profile - * @return
true
if the profile exceeds 1000 bytes,
false
otherwise. - */ - private static boolean exceeds1000CharLineLimit(Profile profile) { - String line = profile.toString(); - return line.getBytes(UTF8Util.UTF8).length > 1000; - } -} diff --git a/sop-java/src/main/kotlin/sop/Profile.kt b/sop-java/src/main/kotlin/sop/Profile.kt new file mode 100644 index 0000000..2125c57 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/Profile.kt @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import sop.util.Optional +import sop.util.UTF8Util + +/** + * Tuple class bundling a profile name and description. + * + * @param name profile name. A profile name is a UTF-8 string that has no whitespace in it. Similar + * to OpenPGP Notation names, profile names are divided into two namespaces: The IETF namespace + * and the user namespace. A profile name in the user namespace ends with the `@` character (0x40) + * followed by a DNS domain name. A profile name in the IETF namespace does not have an `@` + * character. A profile name in the user space is owned and controlled by the owner of the domain + * in the suffix. A profile name in the IETF namespace that begins with the string `rfc` should + * have semantics that hew as closely as possible to the referenced RFC. Similarly, a profile name + * in the IETF namespace that begins with the string `draft-` should have semantics that hew as + * closely as possible to the referenced Internet Draft. + * @param description a free-form description of the profile. + * @see + * SOP Spec - Profile + */ +data class Profile(val name: String, val description: Optional) { + + @JvmOverloads + constructor( + name: String, + description: String? = null + ) : this(name, Optional.ofNullable(description?.trim()?.ifBlank { null })) + + init { + require(name.trim().isNotBlank()) { "Name cannot be empty." } + require(!name.contains(":")) { "Name cannot contain ':'." } + require(listOf(" ", "\n", "\t", "\r").none { name.contains(it) }) { + "Name cannot contain whitespace characters." + } + require(!exceeds1000CharLineLimit(this)) { + "The line representation of a profile MUST NOT exceed 1000 bytes." + } + } + + fun hasDescription() = description.isPresent + + /** + * Convert the profile into a String for displaying. + * + * @return string + */ + override fun toString(): String = + if (description.isEmpty) name else "$name: ${description.get()}" + + companion object { + + /** + * Parse a [Profile] from its string representation. + * + * @param string string representation + * @return profile + */ + @JvmStatic + fun parse(string: String): Profile { + return if (string.contains(": ")) { + Profile( + string.substring(0, string.indexOf(": ")), + string.substring(string.indexOf(": ") + 2).trim()) + } else if (string.endsWith(":")) { + Profile(string.substring(0, string.length - 1)) + } else { + Profile(string.trim()) + } + } + + /** + * Test if the string representation of the profile exceeds the limit of 1000 bytes length. + * + * @param profile profile + * @return `true` if the profile exceeds 1000 bytes, `false` otherwise. + */ + @JvmStatic + private fun exceeds1000CharLineLimit(profile: Profile): Boolean = + profile.toString().toByteArray(UTF8Util.UTF8).size > 1000 + } +} From 0cb5c74a11109cbe7a3b266a95f7655b8ef68441 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 12:48:23 +0100 Subject: [PATCH 067/298] Kotlin conversion: Optional --- sop-java/src/main/java/sop/util/Optional.java | 50 ------------------- sop-java/src/main/kotlin/sop/util/Optional.kt | 26 ++++++++++ 2 files changed, 26 insertions(+), 50 deletions(-) delete mode 100644 sop-java/src/main/java/sop/util/Optional.java create mode 100644 sop-java/src/main/kotlin/sop/util/Optional.kt diff --git a/sop-java/src/main/java/sop/util/Optional.java b/sop-java/src/main/java/sop/util/Optional.java deleted file mode 100644 index 00eb201..0000000 --- a/sop-java/src/main/java/sop/util/Optional.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -/** - * Backport of java.util.Optional for older Android versions. - * - * @param item type - */ -public class Optional { - - private final T item; - - public Optional() { - this(null); - } - - public Optional(T item) { - this.item = item; - } - - public static Optional of(T item) { - if (item == null) { - throw new NullPointerException("Item cannot be null."); - } - return new Optional<>(item); - } - - public static Optional ofNullable(T item) { - return new Optional<>(item); - } - - public static Optional ofEmpty() { - return new Optional<>(null); - } - - public T get() { - return item; - } - - public boolean isPresent() { - return item != null; - } - - public boolean isEmpty() { - return item == null; - } -} diff --git a/sop-java/src/main/kotlin/sop/util/Optional.kt b/sop-java/src/main/kotlin/sop/util/Optional.kt new file mode 100644 index 0000000..0344d0b --- /dev/null +++ b/sop-java/src/main/kotlin/sop/util/Optional.kt @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util + +/** + * Backport of java.util.Optional for older Android versions. + * + * @param item type + */ +data class Optional(val item: T? = null) { + + val isPresent: Boolean = item != null + val isEmpty: Boolean = item == null + + fun get() = item + + companion object { + @JvmStatic fun of(item: T) = Optional(item!!) + + @JvmStatic fun ofNullable(item: T?) = Optional(item) + + @JvmStatic fun ofEmpty() = Optional(null as T?) + } +} From bbe159e88c5442a61ae5dcbe7fca695f408cf785 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 12:56:12 +0100 Subject: [PATCH 068/298] Kotlin conversion: SigningResult --- sop-java/src/main/java/sop/SigningResult.java | 50 ------------------- sop-java/src/main/kotlin/sop/SigningResult.kt | 28 +++++++++++ 2 files changed, 28 insertions(+), 50 deletions(-) delete mode 100644 sop-java/src/main/java/sop/SigningResult.java create mode 100644 sop-java/src/main/kotlin/sop/SigningResult.kt diff --git a/sop-java/src/main/java/sop/SigningResult.java b/sop-java/src/main/java/sop/SigningResult.java deleted file mode 100644 index 1ea1ba8..0000000 --- a/sop-java/src/main/java/sop/SigningResult.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -/** - * This class contains various information about a signed message. - */ -public final class SigningResult { - - private final MicAlg micAlg; - - private SigningResult(MicAlg micAlg) { - this.micAlg = micAlg; - } - - /** - * Return a 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. - * - * @return micalg - */ - public MicAlg getMicAlg() { - return micAlg; - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - - private MicAlg micAlg; - - public Builder setMicAlg(MicAlg micAlg) { - this.micAlg = micAlg; - return this; - } - - public SigningResult build() { - SigningResult signingResult = new SigningResult(micAlg); - return signingResult; - } - } -} diff --git a/sop-java/src/main/kotlin/sop/SigningResult.kt b/sop-java/src/main/kotlin/sop/SigningResult.kt new file mode 100644 index 0000000..29304ea --- /dev/null +++ b/sop-java/src/main/kotlin/sop/SigningResult.kt @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +/** + * This class contains various information about a signed message. + * + * @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. + */ +data class SigningResult(val micAlg: MicAlg) { + + class Builder internal constructor() { + private var micAlg = MicAlg.empty() + + fun setMicAlg(micAlg: MicAlg) = apply { this.micAlg = micAlg } + + fun build() = SigningResult(micAlg) + } + + companion object { + @JvmStatic fun builder() = Builder() + } +} From 9dbb93e13dd14083d48622b2ff2594db41ec9de8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:05:30 +0100 Subject: [PATCH 069/298] Kotlin conversion: MicAlg --- sop-java/src/main/java/sop/MicAlg.java | 55 ---------------------- sop-java/src/main/kotlin/sop/MicAlg.kt | 34 +++++++++++++ sop-java/src/test/java/sop/MicAlgTest.java | 2 +- 3 files changed, 35 insertions(+), 56 deletions(-) delete mode 100644 sop-java/src/main/java/sop/MicAlg.java create mode 100644 sop-java/src/main/kotlin/sop/MicAlg.kt diff --git a/sop-java/src/main/java/sop/MicAlg.java b/sop-java/src/main/java/sop/MicAlg.java deleted file mode 100644 index 5bee787..0000000 --- a/sop-java/src/main/java/sop/MicAlg.java +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.OutputStream; -import java.io.PrintWriter; - -public class MicAlg { - - private final String micAlg; - - public MicAlg(String micAlg) { - if (micAlg == null) { - throw new IllegalArgumentException("MicAlg String cannot be null."); - } - this.micAlg = micAlg; - } - - public static MicAlg empty() { - return new MicAlg(""); - } - - public static MicAlg fromHashAlgorithmId(int id) { - switch (id) { - case 1: - return new MicAlg("pgp-md5"); - case 2: - return new MicAlg("pgp-sha1"); - case 3: - return new MicAlg("pgp-ripemd160"); - case 8: - return new MicAlg("pgp-sha256"); - case 9: - return new MicAlg("pgp-sha384"); - case 10: - return new MicAlg("pgp-sha512"); - case 11: - return new MicAlg("pgp-sha224"); - default: - throw new IllegalArgumentException("Unsupported hash algorithm ID: " + id); - } - } - - public String getMicAlg() { - return micAlg; - } - - public void writeTo(OutputStream outputStream) { - PrintWriter pw = new PrintWriter(outputStream); - pw.write(getMicAlg()); - pw.close(); - } -} diff --git a/sop-java/src/main/kotlin/sop/MicAlg.kt b/sop-java/src/main/kotlin/sop/MicAlg.kt new file mode 100644 index 0000000..58ce7b5 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/MicAlg.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.io.OutputStream +import java.io.PrintWriter + +data class MicAlg(val micAlg: String) { + + fun writeTo(outputStream: OutputStream) { + PrintWriter(outputStream).use { it.write(micAlg) } + } + + companion object { + @JvmStatic fun empty() = MicAlg("") + + @JvmStatic + fun fromHashAlgorithmId(id: Int) = + when (id) { + 1 -> "pgp-md5" + 2 -> "pgp-sha1" + 3 -> "pgp-ripemd160" + 8 -> "pgp-sha256" + 9 -> "pgp-sha384" + 10 -> "pgp-sha512" + 11 -> "pgp-sha224" + 12 -> "pgp-sha3-256" + 14 -> "pgp-sha3-512" + else -> throw IllegalArgumentException("Unsupported hash algorithm ID: $id") + }.let { MicAlg(it) } + } +} diff --git a/sop-java/src/test/java/sop/MicAlgTest.java b/sop-java/src/test/java/sop/MicAlgTest.java index 16f54ef..7c6b30a 100644 --- a/sop-java/src/test/java/sop/MicAlgTest.java +++ b/sop-java/src/test/java/sop/MicAlgTest.java @@ -18,7 +18,7 @@ public class MicAlgTest { @Test public void constructorNullArgThrows() { - assertThrows(IllegalArgumentException.class, () -> new MicAlg(null)); + assertThrows(NullPointerException.class, () -> new MicAlg(null)); } @Test From e6562cecff7e8c444f8d410101a6a2c70378399f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:16:37 +0100 Subject: [PATCH 070/298] Kotlin conversion: Ready --- sop-java/src/main/java/sop/Ready.java | 45 ------------------------ sop-java/src/main/kotlin/sop/Ready.kt | 49 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 45 deletions(-) delete mode 100644 sop-java/src/main/java/sop/Ready.java create mode 100644 sop-java/src/main/kotlin/sop/Ready.kt diff --git a/sop-java/src/main/java/sop/Ready.java b/sop-java/src/main/java/sop/Ready.java deleted file mode 100644 index 71ab26e..0000000 --- a/sop-java/src/main/java/sop/Ready.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public abstract class Ready { - - /** - * Write the data to the provided output stream. - * - * @param outputStream output stream - * @throws IOException in case of an IO error - */ - public abstract void writeTo(OutputStream outputStream) throws IOException; - - /** - * Return the data as a byte array by writing it to a {@link ByteArrayOutputStream} first and then returning - * the array. - * - * @return data as byte array - * @throws IOException in case of an IO error - */ - public byte[] getBytes() throws IOException { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - writeTo(bytes); - return bytes.toByteArray(); - } - - /** - * Return an input stream containing the data. - * - * @return input stream - * @throws IOException in case of an IO error - */ - public InputStream getInputStream() throws IOException { - return new ByteArrayInputStream(getBytes()); - } -} diff --git a/sop-java/src/main/kotlin/sop/Ready.kt b/sop-java/src/main/kotlin/sop/Ready.kt new file mode 100644 index 0000000..1eb3fb3 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/Ready.kt @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +/** Abstract class that encapsulates output data, waiting to be consumed. */ +abstract class Ready { + + /** + * Write the data to the provided output stream. + * + * @param outputStream output stream + * @throws IOException in case of an IO error + */ + @Throws(IOException::class) abstract fun writeTo(outputStream: OutputStream) + + /** + * Return the data as a byte array by writing it to a [ByteArrayOutputStream] first and then + * returning the array. + * + * @return data as byte array + * @throws IOException in case of an IO error + */ + val bytes: ByteArray + @Throws(IOException::class) + get() = + ByteArrayOutputStream() + .let { + writeTo(it) + it + } + .toByteArray() + + /** + * Return an input stream containing the data. + * + * @return input stream + * @throws IOException in case of an IO error + */ + val inputStream: InputStream + @Throws(IOException::class) get() = ByteArrayInputStream(bytes) +} From a89e70c19e63362d52b0946aeacad73e6c1bfdc9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:23:26 +0100 Subject: [PATCH 071/298] Kotlin conversion: ReadyWithResult --- .../src/main/java/sop/ReadyWithResult.java | 41 ------------------- .../src/main/kotlin/sop/ReadyWithResult.kt | 41 +++++++++++++++++++ 2 files changed, 41 insertions(+), 41 deletions(-) delete mode 100644 sop-java/src/main/java/sop/ReadyWithResult.java create mode 100644 sop-java/src/main/kotlin/sop/ReadyWithResult.kt diff --git a/sop-java/src/main/java/sop/ReadyWithResult.java b/sop-java/src/main/java/sop/ReadyWithResult.java deleted file mode 100644 index 9feedda..0000000 --- a/sop-java/src/main/java/sop/ReadyWithResult.java +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -import sop.exception.SOPGPException; - -public abstract class ReadyWithResult { - - /** - * Write the data e.g. decrypted plaintext to the provided output stream and return the result of the - * processing operation. - * - * @param outputStream output stream - * @return result, eg. signatures - * - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature if there are no valid signatures found - */ - public abstract T writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature; - - /** - * Return the data as a {@link ByteArrayAndResult}. - * Calling {@link ByteArrayAndResult#getBytes()} will give you access to the data as byte array, while - * {@link ByteArrayAndResult#getResult()} will grant access to the appended result. - * - * @return byte array and result - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature if there are no valid signatures found - */ - public ByteArrayAndResult toByteArrayAndResult() throws IOException, SOPGPException.NoSignature { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - T result = writeTo(bytes); - return new ByteArrayAndResult<>(bytes.toByteArray(), result); - } -} diff --git a/sop-java/src/main/kotlin/sop/ReadyWithResult.kt b/sop-java/src/main/kotlin/sop/ReadyWithResult.kt new file mode 100644 index 0000000..d309c76 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/ReadyWithResult.kt @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.OutputStream +import sop.exception.SOPGPException + +abstract class ReadyWithResult { + + /** + * Write the data e.g. decrypted plaintext to the provided output stream and return the result + * of the processing operation. + * + * @param outputStream output stream + * @return result, eg. signatures + * @throws IOException in case of an IO error + * @throws SOPGPException in case of a SOP protocol error + */ + @Throws(IOException::class, SOPGPException::class) + abstract fun writeTo(outputStream: OutputStream): T + + /** + * Return the data as a [ByteArrayAndResult]. Calling [ByteArrayAndResult.bytes] will give you + * access to the data as byte array, while [ByteArrayAndResult.result] will grant access to the + * appended result. + * + * @return byte array and result + * @throws IOException in case of an IO error + * @throws SOPGPException.NoSignature if there are no valid signatures found + */ + @Throws(IOException::class, SOPGPException::class) + fun toByteArrayAndResult() = + ByteArrayOutputStream().let { + val result = writeTo(it) + ByteArrayAndResult(it.toByteArray(), result) + } +} From 2391ffc9b2c5e6585f8eb24219e741eb7cfdd031 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:26:33 +0100 Subject: [PATCH 072/298] Kotlin conversion: DecryptionResult --- .../src/main/java/sop/DecryptionResult.java | 29 ------------------- .../src/main/kotlin/sop/DecryptionResult.kt | 16 ++++++++++ 2 files changed, 16 insertions(+), 29 deletions(-) delete mode 100644 sop-java/src/main/java/sop/DecryptionResult.java create mode 100644 sop-java/src/main/kotlin/sop/DecryptionResult.kt diff --git a/sop-java/src/main/java/sop/DecryptionResult.java b/sop-java/src/main/java/sop/DecryptionResult.java deleted file mode 100644 index 4f0e1ab..0000000 --- a/sop-java/src/main/java/sop/DecryptionResult.java +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import sop.util.Optional; - -public class DecryptionResult { - - private final Optional sessionKey; - private final List verifications; - - public DecryptionResult(SessionKey sessionKey, List verifications) { - this.sessionKey = Optional.ofNullable(sessionKey); - this.verifications = Collections.unmodifiableList(verifications); - } - - public Optional getSessionKey() { - return sessionKey; - } - - public List getVerifications() { - return new ArrayList<>(verifications); - } -} diff --git a/sop-java/src/main/kotlin/sop/DecryptionResult.kt b/sop-java/src/main/kotlin/sop/DecryptionResult.kt new file mode 100644 index 0000000..1ba98bb --- /dev/null +++ b/sop-java/src/main/kotlin/sop/DecryptionResult.kt @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import sop.util.Optional + +data class DecryptionResult +internal constructor(val sessionKey: Optional, val verifications: List) { + + constructor( + sessionKey: SessionKey?, + verifications: List + ) : this(Optional.ofNullable(sessionKey), verifications) +} From dc23c8aa98a9f141af6051da1415b82dfe3a9f70 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:29:58 +0100 Subject: [PATCH 073/298] Kotlin conversion: Signatures --- sop-java/src/main/java/sop/Signatures.java | 21 --------------------- sop-java/src/main/kotlin/sop/Signatures.kt | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 21 deletions(-) delete mode 100644 sop-java/src/main/java/sop/Signatures.java create mode 100644 sop-java/src/main/kotlin/sop/Signatures.kt diff --git a/sop-java/src/main/java/sop/Signatures.java b/sop-java/src/main/java/sop/Signatures.java deleted file mode 100644 index dd3f000..0000000 --- a/sop-java/src/main/java/sop/Signatures.java +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.IOException; -import java.io.OutputStream; - -public abstract class Signatures extends Ready { - - /** - * Write OpenPGP signatures to the provided output stream. - * - * @param signatureOutputStream output stream - * @throws IOException in case of an IO error - */ - @Override - public abstract void writeTo(OutputStream signatureOutputStream) throws IOException; - -} diff --git a/sop-java/src/main/kotlin/sop/Signatures.kt b/sop-java/src/main/kotlin/sop/Signatures.kt new file mode 100644 index 0000000..63aafe9 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/Signatures.kt @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.io.IOException +import java.io.OutputStream + +abstract class Signatures : Ready() { + + /** + * Write OpenPGP signatures to the provided output stream. + * + * @param outputStream signature output stream + * @throws IOException in case of an IO error + */ + @Throws(IOException::class) abstract override fun writeTo(outputStream: OutputStream) +} From 31409b7949415b995c5a3a7cf52ad4af40b640bd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:38:53 +0100 Subject: [PATCH 074/298] Kotlin conversion: SOP --- sop-java/src/main/java/sop/SOP.java | 177 ---------------------------- sop-java/src/main/kotlin/sop/SOP.kt | 97 +++++++++++++++ 2 files changed, 97 insertions(+), 177 deletions(-) delete mode 100644 sop-java/src/main/java/sop/SOP.java create mode 100644 sop-java/src/main/kotlin/sop/SOP.kt diff --git a/sop-java/src/main/java/sop/SOP.java b/sop-java/src/main/java/sop/SOP.java deleted file mode 100644 index 1200e21..0000000 --- a/sop-java/src/main/java/sop/SOP.java +++ /dev/null @@ -1,177 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import sop.operation.Armor; -import sop.operation.ChangeKeyPassword; -import sop.operation.Dearmor; -import sop.operation.Decrypt; -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.DetachedSign; -import sop.operation.DetachedVerify; -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 operations. - *
- * Note: Subcommand objects acquired by calling any method of this interface are not intended for reuse. - * If you for example need to generate multiple keys, make a dedicated call to {@link #generateKey()} once per - * key generation. - */ -public interface SOP { - - /** - * Get information about the implementations name and version. - * - * @return version - */ - Version version(); - - /** - * Generate a secret key. - * Customize the operation using the builder {@link GenerateKey}. - * - * @return builder instance - */ - GenerateKey generateKey(); - - /** - * Extract a certificate (public key) from a secret key. - * Customize the operation using the builder {@link ExtractCert}. - * - * @return builder instance - */ - ExtractCert extractCert(); - - /** - * Create detached signatures. - * Customize the operation using the builder {@link DetachedSign}. - *

- * If you want to sign a message inline, use {@link #inlineSign()} instead. - * - * @return builder instance - */ - default DetachedSign sign() { - return detachedSign(); - } - - /** - * Create detached signatures. - * Customize the operation using the builder {@link DetachedSign}. - *

- * If you want to sign a message inline, use {@link #inlineSign()} instead. - * - * @return builder instance - */ - DetachedSign detachedSign(); - - /** - * Sign a message using inline signatures. - *

- * If you need to create detached signatures, use {@link #detachedSign()} instead. - * - * @return builder instance - */ - InlineSign inlineSign(); - - /** - * Verify detached signatures. - * Customize the operation using the builder {@link DetachedVerify}. - *

- * If you need to verify an inline-signed message, use {@link #inlineVerify()} instead. - * - * @return builder instance - */ - default DetachedVerify verify() { - return detachedVerify(); - } - - /** - * Verify detached signatures. - * Customize the operation using the builder {@link DetachedVerify}. - *

- * If you need to verify an inline-signed message, use {@link #inlineVerify()} instead. - * - * @return builder instance - */ - DetachedVerify detachedVerify(); - - /** - * Verify signatures of an inline-signed message. - *

- * If you need to verify detached signatures over a message, use {@link #detachedVerify()} instead. - * - * @return builder instance - */ - InlineVerify inlineVerify(); - - /** - * Detach signatures from an inline signed message. - * - * @return builder instance - */ - InlineDetach inlineDetach(); - - /** - * Encrypt a message. - * Customize the operation using the builder {@link Encrypt}. - * - * @return builder instance - */ - Encrypt encrypt(); - - /** - * Decrypt a message. - * Customize the operation using the builder {@link Decrypt}. - * - * @return builder instance - */ - Decrypt decrypt(); - - /** - * Convert binary OpenPGP data to ASCII. - * Customize the operation using the builder {@link Armor}. - * - * @return builder instance - */ - Armor armor(); - - /** - * Converts ASCII armored OpenPGP data to binary. - * Customize the operation using the builder {@link Dearmor}. - * - * @return builder instance - */ - Dearmor dearmor(); - - /** - * List supported {@link Profile Profiles} of a subcommand. - * - * @return builder instance - */ - ListProfiles listProfiles(); - - /** - * Revoke one or more secret keys. - * - * @return builder instance - */ - RevokeKey revokeKey(); - - /** - * Update a key's password. - * - * @return builder instance - */ - ChangeKeyPassword changeKeyPassword(); -} diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt new file mode 100644 index 0000000..e01763a --- /dev/null +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +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.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 + * operations. Note: Subcommand objects acquired by calling any method of this interface are not + * 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 + + /** Generate a secret key. */ + fun generateKey(): GenerateKey + + /** Extract a certificate (public key) from a secret key. */ + fun extractCert(): ExtractCert + + /** + * Create detached signatures. If you want to sign a message inline, use [inlineSign] instead. + */ + fun sign(): DetachedSign = detachedSign() + + /** + * Create detached signatures. If you want to sign a message inline, use [inlineSign] instead. + */ + fun detachedSign(): DetachedSign + + /** + * Sign a message using inline signatures. If you need to create detached signatures, use + * [detachedSign] instead. + */ + 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 + + /** Encrypt a message. */ + fun encrypt(): Encrypt + + /** Decrypt a message. */ + fun decrypt(): Decrypt + + /** Convert binary OpenPGP data to ASCII. */ + fun armor(): Armor + + /** Converts ASCII armored OpenPGP data to binary. */ + fun dearmor(): Dearmor + + /** List supported [Profiles][Profile] of a subcommand. */ + fun listProfiles(): ListProfiles + + /** Revoke one or more secret keys. */ + fun revokeKey(): RevokeKey + + /** Update a key's password. */ + fun changeKeyPassword(): ChangeKeyPassword +} From e68d6df57f86da9b4881205fe7f6b455d25c8690 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:50:46 +0100 Subject: [PATCH 075/298] Kotlin conversion: AbstractSign --- .../main/java/sop/operation/AbstractSign.java | 85 ------------------- .../main/kotlin/sop/operation/AbstractSign.kt | 79 +++++++++++++++++ 2 files changed, 79 insertions(+), 85 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/AbstractSign.java create mode 100644 sop-java/src/main/kotlin/sop/operation/AbstractSign.kt diff --git a/sop-java/src/main/java/sop/operation/AbstractSign.java b/sop-java/src/main/java/sop/operation/AbstractSign.java deleted file mode 100644 index 508d741..0000000 --- a/sop-java/src/main/java/sop/operation/AbstractSign.java +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.exception.SOPGPException; -import sop.util.UTF8Util; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -public interface AbstractSign { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - T noArmor(); - - /** - * Add one or more signing keys. - * - * @param key input stream containing encoded keys - * @return builder instance - * - * @throws sop.exception.SOPGPException.KeyCannotSign if the key cannot be used for signing - * @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not contain an OpenPGP key - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm - * @throws IOException in case of an IO error - */ - T key(InputStream key) - throws SOPGPException.KeyCannotSign, - SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo, - IOException; - - /** - * Add one or more signing keys. - * - * @param key byte array containing encoded keys - * @return builder instance - * - * @throws sop.exception.SOPGPException.KeyCannotSign if the key cannot be used for signing - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP key - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm - * @throws IOException in case of an IO error - */ - default T key(byte[] key) - throws SOPGPException.KeyCannotSign, - SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo, - IOException { - return key(new ByteArrayInputStream(key)); - } - - /** - * Provide the password for the secret key used for signing. - * - * @param password password - * @return builder instance - * @throws sop.exception.SOPGPException.UnsupportedOption if key passwords are not supported - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the provided passphrase is not human-readable - */ - default T withKeyPassword(String password) - throws SOPGPException.UnsupportedOption, - SOPGPException.PasswordNotHumanReadable { - return withKeyPassword(password.getBytes(UTF8Util.UTF8)); - } - - /** - * Provide the password for the secret key used for signing. - * - * @param password password - * @return builder instance - * @throws sop.exception.SOPGPException.UnsupportedOption if key passwords are not supported - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the provided passphrase is not human-readable - */ - T withKeyPassword(byte[] password) - throws SOPGPException.UnsupportedOption, - SOPGPException.PasswordNotHumanReadable; - -} diff --git a/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt b/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt new file mode 100644 index 0000000..0258432 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.KeyCannotSign +import sop.exception.SOPGPException.PasswordNotHumanReadable +import sop.exception.SOPGPException.UnsupportedAsymmetricAlgo +import sop.exception.SOPGPException.UnsupportedOption +import sop.util.UTF8Util + +/** + * Interface for signing operations. + * + * @param builder subclass + */ +interface AbstractSign { + + /** + * Disable ASCII armor encoding. + * + * @return builder instance + */ + fun noArmor(): T + + /** + * Add one or more signing keys. + * + * @param key input stream containing encoded keys + * @return builder instance + * @throws KeyCannotSign if the key cannot be used for signing + * @throws BadData if the [InputStream] does not contain an OpenPGP key + * @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm + * @throws IOException in case of an IO error + */ + @Throws( + KeyCannotSign::class, BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) + fun key(key: InputStream): T + + /** + * Add one or more signing keys. + * + * @param key byte array containing encoded keys + * @return builder instance + * @throws KeyCannotSign if the key cannot be used for signing + * @throws BadData if the byte array does not contain an OpenPGP key + * @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm + * @throws IOException in case of an IO error + */ + @Throws( + KeyCannotSign::class, BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) + fun key(key: ByteArray): T = key(key.inputStream()) + + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if key passwords are not supported + * @throws PasswordNotHumanReadable if the provided passphrase is not human-readable + */ + @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + fun withKeyPassword(password: String): T = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if key passwords are not supported + * @throws PasswordNotHumanReadable if the provided passphrase is not human-readable + */ + @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + fun withKeyPassword(password: ByteArray): T +} From 08ddc5d8a529aa1acfb65b7e849a82b2ca596534 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:51:30 +0100 Subject: [PATCH 076/298] Kotlin conversion: AbstractVerify --- .../java/sop/operation/AbstractVerify.java | 68 ------------------- .../kotlin/sop/operation/AbstractVerify.kt | 57 ++++++++++++++++ 2 files changed, 57 insertions(+), 68 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/AbstractVerify.java create mode 100644 sop-java/src/main/kotlin/sop/operation/AbstractVerify.kt diff --git a/sop-java/src/main/java/sop/operation/AbstractVerify.java b/sop-java/src/main/java/sop/operation/AbstractVerify.java deleted file mode 100644 index 51d84b7..0000000 --- a/sop-java/src/main/java/sop/operation/AbstractVerify.java +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Date; - -/** - * Common API methods shared between verification of inline signatures ({@link InlineVerify}) - * and verification of detached signatures ({@link DetachedVerify}). - * - * @param Builder type ({@link DetachedVerify}, {@link InlineVerify}) - */ -public interface AbstractVerify { - - /** - * Makes the SOP implementation consider signatures before this date invalid. - * - * @param timestamp timestamp - * @return builder instance - */ - T notBefore(Date timestamp) - throws SOPGPException.UnsupportedOption; - - /** - * Makes the SOP implementation consider signatures after this date invalid. - * - * @param timestamp timestamp - * @return builder instance - */ - T notAfter(Date timestamp) - throws SOPGPException.UnsupportedOption; - - /** - * Add one or more verification cert. - * - * @param cert input stream containing the encoded certs - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the input stream does not contain an OpenPGP certificate - * @throws IOException in case of an IO error - */ - T cert(InputStream cert) - throws SOPGPException.BadData, - IOException; - - /** - * Add one or more verification cert. - * - * @param cert byte array containing the encoded certs - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP certificate - * @throws IOException in case of an IO error - */ - default T cert(byte[] cert) - throws SOPGPException.BadData, - IOException { - return cert(new ByteArrayInputStream(cert)); - } - -} diff --git a/sop-java/src/main/kotlin/sop/operation/AbstractVerify.kt b/sop-java/src/main/kotlin/sop/operation/AbstractVerify.kt new file mode 100644 index 0000000..3554ea3 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/AbstractVerify.kt @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import java.util.* +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.UnsupportedOption + +/** + * Common API methods shared between verification of inline signatures ([InlineVerify]) and + * verification of detached signatures ([DetachedVerify]). + * + * @param Builder type ([DetachedVerify], [InlineVerify]) + */ +interface AbstractVerify { + + /** + * Makes the SOP implementation consider signatures before this date invalid. + * + * @param timestamp timestamp + * @return builder instance + */ + @Throws(UnsupportedOption::class) fun notBefore(timestamp: Date): T + + /** + * Makes the SOP implementation consider signatures after this date invalid. + * + * @param timestamp timestamp + * @return builder instance + */ + @Throws(UnsupportedOption::class) fun notAfter(timestamp: Date): T + + /** + * Add one or more verification cert. + * + * @param cert input stream containing the encoded certs + * @return builder instance + * @throws BadData if the input stream does not contain an OpenPGP certificate + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) fun cert(cert: InputStream): T + + /** + * Add one or more verification cert. + * + * @param cert byte array containing the encoded certs + * @return builder instance + * @throws BadData if the byte array does not contain an OpenPGP certificate + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) + fun cert(cert: ByteArray): T = cert(cert.inputStream()) +} From 4a123a198078f81c80640716d51c574a956dccda Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:54:24 +0100 Subject: [PATCH 077/298] Kotlin conversion: Armor --- .../src/main/java/sop/operation/Armor.java | 53 ------------------- .../src/main/kotlin/sop/operation/Armor.kt | 46 ++++++++++++++++ 2 files changed, 46 insertions(+), 53 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/Armor.java create mode 100644 sop-java/src/main/kotlin/sop/operation/Armor.kt diff --git a/sop-java/src/main/java/sop/operation/Armor.java b/sop-java/src/main/java/sop/operation/Armor.java deleted file mode 100644 index a625808..0000000 --- a/sop-java/src/main/java/sop/operation/Armor.java +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; - -public interface Armor { - - /** - * Overrides automatic detection of label. - * - * @param label armor label - * @return builder instance - */ - Armor label(ArmorLabel label) - throws SOPGPException.UnsupportedOption; - - /** - * Armor the provided data. - * - * @param data input stream of unarmored OpenPGP data - * @return armored data - * - * @throws sop.exception.SOPGPException.BadData if the data appears to be OpenPGP packets, but those are broken - * @throws IOException in case of an IO error - */ - Ready data(InputStream data) - throws SOPGPException.BadData, - IOException; - - /** - * Armor the provided data. - * - * @param data unarmored OpenPGP data - * @return armored data - * - * @throws sop.exception.SOPGPException.BadData if the data appears to be OpenPGP packets, but those are broken - * @throws IOException in case of an IO error - */ - default Ready data(byte[] data) - throws SOPGPException.BadData, - IOException { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/Armor.kt b/sop-java/src/main/kotlin/sop/operation/Armor.kt new file mode 100644 index 0000000..e89708b --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/Armor.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +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. + * + * @param data input stream of unarmored OpenPGP data + * @return armored data + * @throws BadData if the data appears to be OpenPGP packets, but those are broken + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) fun data(data: InputStream): Ready + + /** + * Armor the provided data. + * + * @param data unarmored OpenPGP data + * @return armored data + * @throws BadData if the data appears to be OpenPGP packets, but those are broken + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) + fun data(data: ByteArray): Ready = data(data.inputStream()) +} From 34e1d8992f2b6adb626dd25dd6c940a233a687bc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:00:49 +0100 Subject: [PATCH 078/298] Kotlin conversion: ChangeKeyPassword --- .../java/sop/operation/ChangeKeyPassword.java | 83 ---------------- .../kotlin/sop/operation/ChangeKeyPassword.kt | 95 +++++++++++++++++++ 2 files changed, 95 insertions(+), 83 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/ChangeKeyPassword.java create mode 100644 sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt diff --git a/sop-java/src/main/java/sop/operation/ChangeKeyPassword.java b/sop-java/src/main/java/sop/operation/ChangeKeyPassword.java deleted file mode 100644 index 460a20a..0000000 --- a/sop-java/src/main/java/sop/operation/ChangeKeyPassword.java +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-FileCopyrightText: 2023 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.ByteArrayInputStream; -import java.io.InputStream; -import java.nio.charset.CharacterCodingException; - -public interface ChangeKeyPassword { - - /** - * Disable ASCII armoring of the output. - * - * @return builder instance - */ - ChangeKeyPassword noArmor(); - - default ChangeKeyPassword oldKeyPassphrase(byte[] password) { - try { - return oldKeyPassphrase(UTF8Util.decodeUTF8(password)); - } catch (CharacterCodingException e) { - throw new SOPGPException.PasswordNotHumanReadable("Password MUST be a valid UTF8 string."); - } - } - - /** - * Provide a passphrase to unlock the secret key. - * This method can be provided multiple times to provide separate passphrases that are tried as a - * means to unlock any secret key material encountered. - * - * @param oldPassphrase old passphrase - * @return builder instance - */ - ChangeKeyPassword oldKeyPassphrase(String oldPassphrase); - - /** - * Provide a passphrase to re-lock the secret key with. - * This method can only be used once, and all key material encountered will be encrypted with the given passphrase. - * If this method is not called, the key material will not be protected. - * - * @param newPassphrase new passphrase - * @return builder instance - */ - default ChangeKeyPassword newKeyPassphrase(byte[] newPassphrase) { - try { - return newKeyPassphrase(UTF8Util.decodeUTF8(newPassphrase)); - } catch (CharacterCodingException e) { - throw new SOPGPException.PasswordNotHumanReadable("Password MUST be a valid UTF8 string."); - } - } - - /** - * Provide a passphrase to re-lock the secret key with. - * This method can only be used once, and all key material encountered will be encrypted with the given passphrase. - * If this method is not called, the key material will not be protected. - * - * @param newPassphrase new passphrase - * @return builder instance - */ - ChangeKeyPassword newKeyPassphrase(String newPassphrase); - - default Ready keys(byte[] keys) throws SOPGPException.KeyIsProtected, SOPGPException.BadData { - return keys(new ByteArrayInputStream(keys)); - } - - /** - * Provide the key material. - * - * @param inputStream input stream of secret key material - * @return ready - * - * @throws sop.exception.SOPGPException.KeyIsProtected if any (sub-) key encountered cannot be unlocked. - * @throws sop.exception.SOPGPException.BadData if the key material is malformed - */ - Ready keys(InputStream inputStream) throws SOPGPException.KeyIsProtected, SOPGPException.BadData; - -} diff --git a/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt b/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt new file mode 100644 index 0000000..224e0f4 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.InputStream +import sop.Ready +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.KeyIsProtected +import sop.exception.SOPGPException.PasswordNotHumanReadable +import sop.util.UTF8Util + +interface ChangeKeyPassword { + + /** + * Disable ASCII armoring of the output. + * + * @return builder instance + */ + fun noArmor(): ChangeKeyPassword + + /** + * Provide a passphrase to unlock the secret key. This method can be provided multiple times to + * provide separate passphrases that are tried as a means to unlock any secret key material + * encountered. + * + * @param oldPassphrase old passphrase + * @return builder instance + */ + @Throws(PasswordNotHumanReadable::class) + fun oldKeyPassphrase(oldPassphrase: ByteArray): ChangeKeyPassword = + try { + oldKeyPassphrase(UTF8Util.decodeUTF8(oldPassphrase)) + } catch (e: CharacterCodingException) { + throw PasswordNotHumanReadable("Password MUST be a valid UTF8 string.") + } + + /** + * Provide a passphrase to unlock the secret key. This method can be provided multiple times to + * provide separate passphrases that are tried as a means to unlock any secret key material + * encountered. + * + * @param oldPassphrase old passphrase + * @return builder instance + */ + fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword + + /** + * Provide a passphrase to re-lock the secret key with. This method can only be used once, and + * all key material encountered will be encrypted with the given passphrase. If this method is + * not called, the key material will not be protected. + * + * @param newPassphrase new passphrase + * @return builder instance + */ + @Throws(PasswordNotHumanReadable::class) + fun newKeyPassphrase(newPassphrase: ByteArray): ChangeKeyPassword = + try { + newKeyPassphrase(UTF8Util.decodeUTF8(newPassphrase)) + } catch (e: CharacterCodingException) { + throw PasswordNotHumanReadable("Password MUST be a valid UTF8 string.") + } + + /** + * Provide a passphrase to re-lock the secret key with. This method can only be used once, and + * all key material encountered will be encrypted with the given passphrase. If this method is + * not called, the key material will not be protected. + * + * @param newPassphrase new passphrase + * @return builder instance + */ + fun newKeyPassphrase(newPassphrase: String): ChangeKeyPassword + + /** + * Provide the key material. + * + * @param keys input stream of secret key material + * @return ready + * @throws KeyIsProtected if any (sub-) key encountered cannot be unlocked. + * @throws BadData if the key material is malformed + */ + @Throws(KeyIsProtected::class, BadData::class) + fun keys(keys: ByteArray): Ready = keys(keys.inputStream()) + + /** + * Provide the key material. + * + * @param keys input stream of secret key material + * @return ready + * @throws KeyIsProtected if any (sub-) key encountered cannot be unlocked. + * @throws BadData if the key material is malformed + */ + @Throws(KeyIsProtected::class, BadData::class) fun keys(keys: InputStream): Ready +} From 39c222dfc893d780d8daaee8fd47854ecb17d143 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:03:18 +0100 Subject: [PATCH 079/298] Kotlin conversion: Dearmor --- .../src/main/java/sop/operation/Dearmor.java | 59 ------------------- .../src/main/kotlin/sop/operation/Dearmor.kt | 46 +++++++++++++++ 2 files changed, 46 insertions(+), 59 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/Dearmor.java create mode 100644 sop-java/src/main/kotlin/sop/operation/Dearmor.kt diff --git a/sop-java/src/main/java/sop/operation/Dearmor.java b/sop-java/src/main/java/sop/operation/Dearmor.java deleted file mode 100644 index 524dc8c..0000000 --- a/sop-java/src/main/java/sop/operation/Dearmor.java +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.exception.SOPGPException; -import sop.util.UTF8Util; - -public interface Dearmor { - - /** - * Dearmor armored OpenPGP data. - * - * @param data armored OpenPGP data - * @return input stream of unarmored data - * - * @throws SOPGPException.BadData in case of non-OpenPGP data - * @throws IOException in case of an IO error - */ - Ready data(InputStream data) - throws SOPGPException.BadData, - IOException; - - /** - * Dearmor armored OpenPGP data. - * - * @param data armored OpenPGP data - * @return input stream of unarmored data - * - * @throws SOPGPException.BadData in case of non-OpenPGP data - * @throws IOException in case of an IO error - */ - default Ready data(byte[] data) - throws SOPGPException.BadData, - IOException { - return data(new ByteArrayInputStream(data)); - } - - /** - * Dearmor amored OpenPGP data. - * - * @param data armored OpenPGP data - * @return input stream of unarmored data - * - * @throws SOPGPException.BadData in case of non-OpenPGP data - * @throws IOException in case of an IO error - */ - default Ready data(String data) - throws SOPGPException.BadData, - IOException { - return data(data.getBytes(UTF8Util.UTF8)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/Dearmor.kt b/sop-java/src/main/kotlin/sop/operation/Dearmor.kt new file mode 100644 index 0000000..cc5e98d --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/Dearmor.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.Ready +import sop.exception.SOPGPException.BadData +import sop.util.UTF8Util + +interface Dearmor { + + /** + * Dearmor armored OpenPGP data. + * + * @param data armored OpenPGP data + * @return input stream of unarmored data + * @throws BadData in case of non-OpenPGP data + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) fun data(data: InputStream): Ready + + /** + * Dearmor armored OpenPGP data. + * + * @param data armored OpenPGP data + * @return input stream of unarmored data + * @throws BadData in case of non-OpenPGP data + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) + fun data(data: ByteArray): Ready = data(data.inputStream()) + + /** + * Dearmor amored OpenPGP data. + * + * @param data armored OpenPGP data + * @return input stream of unarmored data + * @throws BadData in case of non-OpenPGP data + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) + fun data(data: String): Ready = data(data.toByteArray(UTF8Util.UTF8)) +} From 91a861b5c366d54ddff4b1625e920e0e75d476cb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:08:03 +0100 Subject: [PATCH 080/298] Kotlin conversion: Decrypt --- .../src/main/java/sop/operation/Decrypt.java | 193 ------------------ .../src/main/kotlin/sop/operation/Decrypt.kt | 169 +++++++++++++++ 2 files changed, 169 insertions(+), 193 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/Decrypt.java create mode 100644 sop-java/src/main/kotlin/sop/operation/Decrypt.kt diff --git a/sop-java/src/main/java/sop/operation/Decrypt.java b/sop-java/src/main/java/sop/operation/Decrypt.java deleted file mode 100644 index 0123bbc..0000000 --- a/sop-java/src/main/java/sop/operation/Decrypt.java +++ /dev/null @@ -1,193 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.exception.SOPGPException; -import sop.util.UTF8Util; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Date; - -public interface Decrypt { - - /** - * Makes the SOP consider signatures before this date invalid. - * - * @param timestamp timestamp - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - Decrypt verifyNotBefore(Date timestamp) - throws SOPGPException.UnsupportedOption; - - /** - * Makes the SOP consider signatures after this date invalid. - * - * @param timestamp timestamp - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - Decrypt verifyNotAfter(Date timestamp) - throws SOPGPException.UnsupportedOption; - - /** - * Adds one or more verification cert. - * - * @param cert input stream containing the cert(s) - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the {@link InputStream} doesn't provide an OpenPGP certificate - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the cert uses an unsupported asymmetric algorithm - * @throws IOException in case of an IO error - */ - Decrypt verifyWithCert(InputStream cert) - throws SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo, - IOException; - - /** - * Adds one or more verification cert. - * - * @param cert byte array containing the cert(s) - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the byte array doesn't contain an OpenPGP certificate - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the cert uses an unsupported asymmetric algorithm - * @throws IOException in case of an IO error - */ - default Decrypt verifyWithCert(byte[] cert) - throws SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo, - IOException { - return verifyWithCert(new ByteArrayInputStream(cert)); - } - - /** - * Tries to decrypt with the given session key. - * - * @param sessionKey session key - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - Decrypt withSessionKey(SessionKey sessionKey) - throws SOPGPException.UnsupportedOption; - - /** - * Tries to decrypt with the given password. - * - * @param password password - * @return builder instance - * - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - Decrypt withPassword(String password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption; - - /** - * Adds one or more decryption key. - * - * @param key input stream containing the key(s) - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not provide an OpenPGP key - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm - * @throws IOException in case of an IO error - */ - Decrypt withKey(InputStream key) - throws SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo, - IOException; - - /** - * Adds one or more decryption key. - * - * @param key byte array containing the key(s) - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP key - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm - * @throws IOException in case of an IO error - */ - default Decrypt withKey(byte[] key) - throws SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo, - IOException { - return withKey(new ByteArrayInputStream(key)); - } - - /** - * Provide the decryption password for the secret key. - * - * @param password password - * @return builder instance - * @throws sop.exception.SOPGPException.UnsupportedOption if the implementation does not support key passwords - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - */ - default Decrypt withKeyPassword(String password) - throws SOPGPException.UnsupportedOption, - SOPGPException.PasswordNotHumanReadable { - return withKeyPassword(password.getBytes(UTF8Util.UTF8)); - } - - /** - * Provide the decryption password for the secret key. - * - * @param password password - * @return builder instance - * @throws sop.exception.SOPGPException.UnsupportedOption if the implementation does not support key passwords - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - */ - Decrypt withKeyPassword(byte[] password) - throws SOPGPException.UnsupportedOption, - SOPGPException.PasswordNotHumanReadable; - - /** - * Decrypts the given ciphertext, returning verification results and plaintext. - * @param ciphertext ciphertext - * @return ready with result - * - * @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not provide an OpenPGP message - * @throws sop.exception.SOPGPException.MissingArg if an argument required for decryption was not provided - * @throws sop.exception.SOPGPException.CannotDecrypt in case decryption fails for some reason - * @throws sop.exception.SOPGPException.KeyIsProtected if the decryption key cannot be unlocked (e.g. missing passphrase) - * @throws IOException in case of an IO error - */ - ReadyWithResult ciphertext(InputStream ciphertext) - throws SOPGPException.BadData, - SOPGPException.MissingArg, - SOPGPException.CannotDecrypt, - SOPGPException.KeyIsProtected, - IOException; - - /** - * Decrypts the given ciphertext, returning verification results and plaintext. - * @param ciphertext ciphertext - * @return ready with result - * - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an encrypted OpenPGP message - * @throws sop.exception.SOPGPException.MissingArg in case of missing decryption method (password or key required) - * @throws sop.exception.SOPGPException.CannotDecrypt in case decryption fails for some reason - * @throws sop.exception.SOPGPException.KeyIsProtected if the decryption key cannot be unlocked (e.g. missing passphrase) - * @throws IOException in case of an IO error - */ - default ReadyWithResult ciphertext(byte[] ciphertext) - throws SOPGPException.BadData, - SOPGPException.MissingArg, - SOPGPException.CannotDecrypt, - SOPGPException.KeyIsProtected, - IOException { - return ciphertext(new ByteArrayInputStream(ciphertext)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/Decrypt.kt b/sop-java/src/main/kotlin/sop/operation/Decrypt.kt new file mode 100644 index 0000000..9c24c54 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/Decrypt.kt @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import java.util.* +import sop.DecryptionResult +import sop.ReadyWithResult +import sop.SessionKey +import sop.exception.SOPGPException.* +import sop.util.UTF8Util + +interface Decrypt { + + /** + * Makes the SOP consider signatures before this date invalid. + * + * @param timestamp timestamp + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun verifyNotBefore(timestamp: Date): Decrypt + + /** + * Makes the SOP consider signatures after this date invalid. + * + * @param timestamp timestamp + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun verifyNotAfter(timestamp: Date): Decrypt + + /** + * Adds one or more verification cert. + * + * @param cert input stream containing the cert(s) + * @return builder instance + * @throws BadData if the [InputStream] doesn't provide an OpenPGP certificate + * @throws UnsupportedAsymmetricAlgo if the cert uses an unsupported asymmetric algorithm + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) + fun verifyWithCert(cert: InputStream): Decrypt + + /** + * Adds one or more verification cert. + * + * @param cert byte array containing the cert(s) + * @return builder instance + * @throws BadData if the byte array doesn't contain an OpenPGP certificate + * @throws UnsupportedAsymmetricAlgo if the cert uses an unsupported asymmetric algorithm + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) + fun verifyWithCert(cert: ByteArray): Decrypt = verifyWithCert(cert.inputStream()) + + /** + * Tries to decrypt with the given session key. + * + * @param sessionKey session key + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun withSessionKey(sessionKey: SessionKey): Decrypt + + /** + * Tries to decrypt with the given password. + * + * @param password password + * @return builder instance + * @throws PasswordNotHumanReadable if the password is not human-readable + * @throws UnsupportedOption if this option is not supported + */ + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) + fun withPassword(password: String): Decrypt + + /** + * Adds one or more decryption key. + * + * @param key input stream containing the key(s) + * @return builder instance + * @throws BadData if the [InputStream] does not provide an OpenPGP key + * @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) + fun withKey(key: InputStream): Decrypt + + /** + * Adds one or more decryption key. + * + * @param key byte array containing the key(s) + * @return builder instance + * @throws BadData if the byte array does not contain an OpenPGP key + * @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) + fun withKey(key: ByteArray): Decrypt { + return withKey(key.inputStream()) + } + + /** + * Provide the decryption password for the secret key. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if the implementation does not support key passwords + * @throws PasswordNotHumanReadable if the password is not human-readable + */ + @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + fun withKeyPassword(password: String): Decrypt { + return withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + } + + /** + * Provide the decryption password for the secret key. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if the implementation does not support key passwords + * @throws PasswordNotHumanReadable if the password is not human-readable + */ + @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + fun withKeyPassword(password: ByteArray): Decrypt + + /** + * Decrypts the given ciphertext, returning verification results and plaintext. + * + * @param ciphertext ciphertext + * @return ready with result + * @throws BadData if the [InputStream] does not provide an OpenPGP message + * @throws MissingArg if an argument required for decryption was not provided + * @throws CannotDecrypt in case decryption fails for some reason + * @throws KeyIsProtected if the decryption key cannot be unlocked (e.g. missing passphrase) + * @throws IOException in case of an IO error + */ + @Throws( + BadData::class, + MissingArg::class, + CannotDecrypt::class, + KeyIsProtected::class, + IOException::class) + fun ciphertext(ciphertext: InputStream): ReadyWithResult + + /** + * Decrypts the given ciphertext, returning verification results and plaintext. + * + * @param ciphertext ciphertext + * @return ready with result + * @throws BadData if the byte array does not contain an encrypted OpenPGP message + * @throws MissingArg in case of missing decryption method (password or key required) + * @throws CannotDecrypt in case decryption fails for some reason + * @throws KeyIsProtected if the decryption key cannot be unlocked (e.g. missing passphrase) + * @throws IOException in case of an IO error + */ + @Throws( + BadData::class, + MissingArg::class, + CannotDecrypt::class, + KeyIsProtected::class, + IOException::class) + fun ciphertext(ciphertext: ByteArray): ReadyWithResult { + return ciphertext(ciphertext.inputStream()) + } +} From 4dc1779a06be4aadffdd39855cb6e4da081cde00 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:10:06 +0100 Subject: [PATCH 081/298] Kotlin conversion: DetachedSign --- .../main/java/sop/operation/DetachedSign.java | 61 ------------------- .../main/kotlin/sop/operation/DetachedSign.kt | 50 +++++++++++++++ 2 files changed, 50 insertions(+), 61 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/DetachedSign.java create mode 100644 sop-java/src/main/kotlin/sop/operation/DetachedSign.kt diff --git a/sop-java/src/main/java/sop/operation/DetachedSign.java b/sop-java/src/main/java/sop/operation/DetachedSign.java deleted file mode 100644 index 745077d..0000000 --- a/sop-java/src/main/java/sop/operation/DetachedSign.java +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.ReadyWithResult; -import sop.SigningResult; -import sop.enums.SignAs; -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -public interface DetachedSign extends AbstractSign { - - /** - * Sets the signature mode. - * Note: This method has to be called before {@link #key(InputStream)} is called. - * - * @param mode signature mode - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - DetachedSign mode(SignAs mode) - throws SOPGPException.UnsupportedOption; - - /** - * Signs data. - * - * @param data input stream containing data - * @return ready - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - * @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered - */ - ReadyWithResult data(InputStream data) - throws IOException, - SOPGPException.KeyIsProtected, - SOPGPException.ExpectedText; - - /** - * Signs data. - * - * @param data byte array containing data - * @return ready - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - * @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered - */ - default ReadyWithResult data(byte[] data) - throws IOException, - SOPGPException.KeyIsProtected, - SOPGPException.ExpectedText { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt b/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt new file mode 100644 index 0000000..c0e62dd --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.ReadyWithResult +import sop.SigningResult +import sop.enums.SignAs +import sop.exception.SOPGPException.* + +interface DetachedSign : AbstractSign { + + /** + * Sets the signature mode. Note: This method has to be called before [key] is called. + * + * @param mode signature mode + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun mode(mode: SignAs): DetachedSign + + /** + * Signs data. + * + * @param data input stream containing data + * @return ready + * @throws IOException in case of an IO error + * @throws KeyIsProtected if at least one signing key cannot be unlocked + * @throws ExpectedText if text data was expected, but binary data was encountered + */ + @Throws(IOException::class, KeyIsProtected::class, ExpectedText::class) + fun data(data: InputStream): ReadyWithResult + + /** + * Signs data. + * + * @param data byte array containing data + * @return ready + * @throws IOException in case of an IO error + * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be + * unlocked + * @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data + * was encountered + */ + @Throws(IOException::class, KeyIsProtected::class, ExpectedText::class) + fun data(data: ByteArray): ReadyWithResult = data(data.inputStream()) +} From ee6975c7d3ab039b917aef476de44af7f542bef2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:11:11 +0100 Subject: [PATCH 082/298] Decrypt: Use return statement --- sop-java/src/main/kotlin/sop/operation/Decrypt.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/operation/Decrypt.kt b/sop-java/src/main/kotlin/sop/operation/Decrypt.kt index 9c24c54..ae228e9 100644 --- a/sop-java/src/main/kotlin/sop/operation/Decrypt.kt +++ b/sop-java/src/main/kotlin/sop/operation/Decrypt.kt @@ -99,9 +99,7 @@ interface Decrypt { * @throws IOException in case of an IO error */ @Throws(BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) - fun withKey(key: ByteArray): Decrypt { - return withKey(key.inputStream()) - } + fun withKey(key: ByteArray): Decrypt = withKey(key.inputStream()) /** * Provide the decryption password for the secret key. @@ -112,9 +110,8 @@ interface Decrypt { * @throws PasswordNotHumanReadable if the password is not human-readable */ @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) - fun withKeyPassword(password: String): Decrypt { - return withKeyPassword(password.toByteArray(UTF8Util.UTF8)) - } + fun withKeyPassword(password: String): Decrypt = + withKeyPassword(password.toByteArray(UTF8Util.UTF8)) /** * Provide the decryption password for the secret key. @@ -163,7 +160,6 @@ interface Decrypt { CannotDecrypt::class, KeyIsProtected::class, IOException::class) - fun ciphertext(ciphertext: ByteArray): ReadyWithResult { - return ciphertext(ciphertext.inputStream()) - } + fun ciphertext(ciphertext: ByteArray): ReadyWithResult = + ciphertext(ciphertext.inputStream()) } From e681090757312dedcc90cc9fc35ee7f64118c99d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:12:56 +0100 Subject: [PATCH 083/298] Kotlin conversion DetachedVerify --- .../java/sop/operation/DetachedVerify.java | 45 ------------------- .../kotlin/sop/operation/DetachedVerify.kt | 36 +++++++++++++++ 2 files changed, 36 insertions(+), 45 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/DetachedVerify.java create mode 100644 sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt diff --git a/sop-java/src/main/java/sop/operation/DetachedVerify.java b/sop-java/src/main/java/sop/operation/DetachedVerify.java deleted file mode 100644 index 9dee870..0000000 --- a/sop-java/src/main/java/sop/operation/DetachedVerify.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * API for verifying detached signatures. - */ -public interface DetachedVerify extends AbstractVerify, VerifySignatures { - - /** - * Provides the detached signatures. - * @param signatures input stream containing encoded, detached signatures. - * - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the input stream does not contain OpenPGP signatures - * @throws IOException in case of an IO error - */ - VerifySignatures signatures(InputStream signatures) - throws SOPGPException.BadData, - IOException; - - /** - * Provides the detached signatures. - * @param signatures byte array containing encoded, detached signatures. - * - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain OpenPGP signatures - * @throws IOException in case of an IO error - */ - default VerifySignatures signatures(byte[] signatures) - throws SOPGPException.BadData, - IOException { - return signatures(new ByteArrayInputStream(signatures)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt b/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt new file mode 100644 index 0000000..2dd9218 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import sop.exception.SOPGPException.BadData + +interface DetachedVerify : AbstractVerify, VerifySignatures { + + /** + * Provides the detached signatures. + * + * @param signatures input stream containing encoded, detached signatures. + * @return builder instance + * @throws BadData if the input stream does not contain OpenPGP signatures + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) + fun signatures(signatures: InputStream): VerifySignatures + + /** + * Provides the detached signatures. + * + * @param signatures byte array containing encoded, detached signatures. + * @return builder instance + * @throws BadData if the byte array does not contain OpenPGP signatures + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) + fun signatures(signatures: ByteArray): VerifySignatures = + signatures(ByteArrayInputStream(signatures)) +} From 41db9d2ac77d7b8e5c2fe79ae02ec36efb6bc240 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:15:56 +0100 Subject: [PATCH 084/298] Kotlin conversion: Encrypt --- .../src/main/java/sop/operation/Encrypt.java | 193 ------------------ .../src/main/kotlin/sop/operation/Encrypt.kt | 166 +++++++++++++++ 2 files changed, 166 insertions(+), 193 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/Encrypt.java create mode 100644 sop-java/src/main/kotlin/sop/operation/Encrypt.kt diff --git a/sop-java/src/main/java/sop/operation/Encrypt.java b/sop-java/src/main/java/sop/operation/Encrypt.java deleted file mode 100644 index b380d32..0000000 --- a/sop-java/src/main/java/sop/operation/Encrypt.java +++ /dev/null @@ -1,193 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.Profile; -import sop.Ready; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.util.UTF8Util; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -public interface Encrypt { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - Encrypt noArmor(); - - /** - * Sets encryption mode. - * - * @param mode mode - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - Encrypt mode(EncryptAs mode) - throws SOPGPException.UnsupportedOption; - - /** - * Adds the signer key. - * - * @param key input stream containing the encoded signer key - * @return builder instance - * - * @throws sop.exception.SOPGPException.KeyCannotSign if the key cannot be used for signing - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm - * @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not contain an OpenPGP key - * @throws IOException in case of an IO error - */ - Encrypt signWith(InputStream key) - throws SOPGPException.KeyCannotSign, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData, - IOException; - - /** - * Adds the signer key. - * - * @param key byte array containing the encoded signer key - * @return builder instance - * - * @throws sop.exception.SOPGPException.KeyCannotSign if the key cannot be used for signing - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP key - * @throws IOException in case of an IO error - */ - default Encrypt signWith(byte[] key) - throws SOPGPException.KeyCannotSign, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData, - IOException { - return signWith(new ByteArrayInputStream(key)); - } - - /** - * Provide the password for the secret key used for signing. - * - * @param password password - * @return builder instance - * - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - * @throws sop.exception.SOPGPException.UnsupportedOption if key password are not supported - */ - default Encrypt withKeyPassword(String password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption { - return withKeyPassword(password.getBytes(UTF8Util.UTF8)); - } - - /** - * Provide the password for the secret key used for signing. - * - * @param password password - * @return builder instance - * - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - * @throws sop.exception.SOPGPException.UnsupportedOption if key password are not supported - */ - Encrypt withKeyPassword(byte[] password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption; - - /** - * Encrypt with the given password. - * - * @param password password - * @return builder instance - * - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - Encrypt withPassword(String password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption; - - /** - * Encrypt with the given cert. - * - * @param cert input stream containing the encoded cert. - * @return builder instance - * - * @throws sop.exception.SOPGPException.CertCannotEncrypt if the certificate is not encryption capable - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the certificate uses an unsupported asymmetric algorithm - * @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not contain an OpenPGP certificate - * @throws IOException in case of an IO error - */ - Encrypt withCert(InputStream cert) - throws SOPGPException.CertCannotEncrypt, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData, - IOException; - - /** - * Encrypt with the given cert. - * - * @param cert byte array containing the encoded cert. - * @return builder instance - * - * @throws sop.exception.SOPGPException.CertCannotEncrypt if the certificate is not encryption capable - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the certificate uses an unsupported asymmetric algorithm - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP certificate - * @throws IOException in case of an IO error - */ - default Encrypt withCert(byte[] cert) - throws SOPGPException.CertCannotEncrypt, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData, - IOException { - return withCert(new ByteArrayInputStream(cert)); - } - - /** - * Pass in a profile. - * - * @param profile profile - * @return builder instance - */ - default Encrypt profile(Profile profile) { - return profile(profile.getName()); - } - - /** - * Pass in a profile identifier. - * - * @param profileName profile identifier - * @return builder instance - */ - Encrypt profile(String profileName); - - /** - * Encrypt the given data yielding the ciphertext. - * @param plaintext plaintext - * @return input stream containing the ciphertext - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - */ - Ready plaintext(InputStream plaintext) - throws IOException, - SOPGPException.KeyIsProtected; - - /** - * Encrypt the given data yielding the ciphertext. - * @param plaintext plaintext - * @return input stream containing the ciphertext - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - */ - default Ready plaintext(byte[] plaintext) - throws IOException, - SOPGPException.KeyIsProtected { - return plaintext(new ByteArrayInputStream(plaintext)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt new file mode 100644 index 0000000..ad30149 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import sop.Profile +import sop.Ready +import sop.enums.EncryptAs +import sop.exception.SOPGPException.* +import sop.util.UTF8Util + +interface Encrypt { + + /** + * Disable ASCII armor encoding. + * + * @return builder instance + */ + fun noArmor(): Encrypt + + /** + * Sets encryption mode. + * + * @param mode mode + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun mode(mode: EncryptAs): Encrypt + + /** + * Adds the signer key. + * + * @param key input stream containing the encoded signer key + * @return builder instance + * @throws KeyCannotSign if the key cannot be used for signing + * @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm + * @throws BadData if the [InputStream] does not contain an OpenPGP key + * @throws IOException in case of an IO error + */ + @Throws( + KeyCannotSign::class, UnsupportedAsymmetricAlgo::class, BadData::class, IOException::class) + fun signWith(key: InputStream): Encrypt + + /** + * Adds the signer key. + * + * @param key byte array containing the encoded signer key + * @return builder instance + * @throws KeyCannotSign if the key cannot be used for signing + * @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm + * @throws BadData if the byte array does not contain an OpenPGP key + * @throws IOException in case of an IO error + */ + @Throws( + KeyCannotSign::class, UnsupportedAsymmetricAlgo::class, BadData::class, IOException::class) + fun signWith(key: ByteArray): Encrypt = signWith(ByteArrayInputStream(key)) + + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws PasswordNotHumanReadable if the password is not human-readable + * @throws UnsupportedOption if key password are not supported + */ + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) + fun withKeyPassword(password: String): Encrypt = + withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws PasswordNotHumanReadable if the password is not human-readable + * @throws UnsupportedOption if key password are not supported + */ + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) + fun withKeyPassword(password: ByteArray): Encrypt + + /** + * Encrypt with the given password. + * + * @param password password + * @return builder instance + * @throws PasswordNotHumanReadable if the password is not human-readable + * @throws UnsupportedOption if this option is not supported + */ + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) + fun withPassword(password: String): Encrypt + + /** + * Encrypt with the given cert. + * + * @param cert input stream containing the encoded cert. + * @return builder instance + * @throws CertCannotEncrypt if the certificate is not encryption capable + * @throws UnsupportedAsymmetricAlgo if the certificate uses an unsupported asymmetric algorithm + * @throws BadData if the [InputStream] does not contain an OpenPGP certificate + * @throws IOException in case of an IO error + */ + @Throws( + CertCannotEncrypt::class, + UnsupportedAsymmetricAlgo::class, + BadData::class, + IOException::class) + fun withCert(cert: InputStream): Encrypt + + /** + * Encrypt with the given cert. + * + * @param cert byte array containing the encoded cert. + * @return builder instance + * @throws CertCannotEncrypt if the certificate is not encryption capable + * @throws UnsupportedAsymmetricAlgo if the certificate uses an unsupported asymmetric algorithm + * @throws BadData if the byte array does not contain an OpenPGP certificate + * @throws IOException in case of an IO error + */ + @Throws( + CertCannotEncrypt::class, + UnsupportedAsymmetricAlgo::class, + BadData::class, + IOException::class) + fun withCert(cert: ByteArray): Encrypt = withCert(ByteArrayInputStream(cert)) + + /** + * Pass in a profile. + * + * @param profile profile + * @return builder instance + */ + fun profile(profile: Profile): Encrypt = profile(profile.name) + + /** + * Pass in a profile identifier. + * + * @param profileName profile identifier + * @return builder instance + */ + fun profile(profileName: String): Encrypt + + /** + * Encrypt the given data yielding the ciphertext. + * + * @param plaintext plaintext + * @return input stream containing the ciphertext + * @throws IOException in case of an IO error + * @throws KeyIsProtected if at least one signing key cannot be unlocked + */ + @Throws(IOException::class, KeyIsProtected::class) fun plaintext(plaintext: InputStream): Ready + + /** + * Encrypt the given data yielding the ciphertext. + * + * @param plaintext plaintext + * @return input stream containing the ciphertext + * @throws IOException in case of an IO error + * @throws KeyIsProtected if at least one signing key cannot be unlocked + */ + @Throws(IOException::class, KeyIsProtected::class) + fun plaintext(plaintext: ByteArray): Ready = plaintext(ByteArrayInputStream(plaintext)) +} From 653675f730788593566aba75879870d4e39f652e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:17:12 +0100 Subject: [PATCH 085/298] Kotlin conversion: ExtractCert --- .../main/java/sop/operation/ExtractCert.java | 50 ------------------- .../main/kotlin/sop/operation/ExtractCert.kt | 42 ++++++++++++++++ 2 files changed, 42 insertions(+), 50 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/ExtractCert.java create mode 100644 sop-java/src/main/kotlin/sop/operation/ExtractCert.kt diff --git a/sop-java/src/main/java/sop/operation/ExtractCert.java b/sop-java/src/main/java/sop/operation/ExtractCert.java deleted file mode 100644 index a862d33..0000000 --- a/sop-java/src/main/java/sop/operation/ExtractCert.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.exception.SOPGPException; - -public interface ExtractCert { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - ExtractCert noArmor(); - - /** - * Extract the cert(s) from the provided key(s). - * - * @param keyInputStream input stream containing the encoding of one or more OpenPGP keys - * @return result containing the encoding of the keys certs - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not contain an OpenPGP key - */ - Ready key(InputStream keyInputStream) - throws IOException, - SOPGPException.BadData; - - /** - * Extract the cert(s) from the provided key(s). - * - * @param key byte array containing the encoding of one or more OpenPGP key - * @return result containing the encoding of the keys certs - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP key - */ - default Ready key(byte[] key) - throws IOException, - SOPGPException.BadData { - return key(new ByteArrayInputStream(key)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt b/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt new file mode 100644 index 0000000..bd48e08 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import sop.Ready +import sop.exception.SOPGPException.BadData + +interface ExtractCert { + + /** + * Disable ASCII armor encoding. + * + * @return builder instance + */ + fun noArmor(): ExtractCert + + /** + * Extract the cert(s) from the provided key(s). + * + * @param keyInputStream input stream containing the encoding of one or more OpenPGP keys + * @return result containing the encoding of the keys certs + * @throws IOException in case of an IO error + * @throws BadData if the [InputStream] does not contain an OpenPGP key + */ + @Throws(IOException::class, BadData::class) fun key(keyInputStream: InputStream): Ready + + /** + * Extract the cert(s) from the provided key(s). + * + * @param key byte array containing the encoding of one or more OpenPGP key + * @return result containing the encoding of the keys certs + * @throws IOException in case of an IO error + * @throws BadData if the byte array does not contain an OpenPGP key + */ + @Throws(IOException::class, BadData::class) + fun key(key: ByteArray): Ready = key(ByteArrayInputStream(key)) +} From 3e6ebe1cc44968b1ecad4d6244fc43f874a2aa64 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:19:08 +0100 Subject: [PATCH 086/298] Kotlin conversion: GenerateKey --- .../main/java/sop/operation/GenerateKey.java | 103 ------------------ .../main/kotlin/sop/operation/GenerateKey.kt | 91 ++++++++++++++++ 2 files changed, 91 insertions(+), 103 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/GenerateKey.java create mode 100644 sop-java/src/main/kotlin/sop/operation/GenerateKey.kt diff --git a/sop-java/src/main/java/sop/operation/GenerateKey.java b/sop-java/src/main/java/sop/operation/GenerateKey.java deleted file mode 100644 index 77afea1..0000000 --- a/sop-java/src/main/java/sop/operation/GenerateKey.java +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.CharacterCodingException; - -import sop.Profile; -import sop.Ready; -import sop.exception.SOPGPException; -import sop.util.UTF8Util; - -public interface GenerateKey { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - GenerateKey noArmor(); - - /** - * Adds a user-id. - * - * @param userId user-id - * @return builder instance - */ - GenerateKey userId(String userId); - - /** - * Set a password for the key. - * - * @param password password to protect the key - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if key passwords are not supported - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - */ - GenerateKey withKeyPassword(String password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption; - - /** - * Set a password for the key. - * - * @param password password to protect the key - * @return builder instance - * - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - * @throws sop.exception.SOPGPException.UnsupportedOption if key passwords are not supported - */ - default GenerateKey withKeyPassword(byte[] password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption { - try { - return withKeyPassword(UTF8Util.decodeUTF8(password)); - } catch (CharacterCodingException e) { - throw new SOPGPException.PasswordNotHumanReadable(); - } - } - - /** - * Pass in a profile. - * - * @param profile profile - * @return builder instance - */ - default GenerateKey profile(Profile profile) { - return profile(profile.getName()); - } - - /** - * Pass in a profile identifier. - * - * @param profile profile identifier - * @return builder instance - */ - GenerateKey profile(String profile); - - /** - * If this options is set, the generated key will not be capable of encryption / decryption. - * - * @return builder instance - */ - GenerateKey signingOnly(); - - /** - * Generate the OpenPGP key and return it encoded as an {@link InputStream}. - * - * @return key - * - * @throws sop.exception.SOPGPException.MissingArg if no user-id was provided - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the generated key uses an unsupported asymmetric algorithm - * @throws IOException in case of an IO error - */ - Ready generate() - throws SOPGPException.MissingArg, - SOPGPException.UnsupportedAsymmetricAlgo, - IOException; -} diff --git a/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt b/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt new file mode 100644 index 0000000..3b83b99 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import sop.Profile +import sop.Ready +import sop.exception.SOPGPException.* +import sop.util.UTF8Util + +interface GenerateKey { + + /** + * Disable ASCII armor encoding. + * + * @return builder instance + */ + fun noArmor(): GenerateKey + + /** + * Adds a user-id. + * + * @param userId user-id + * @return builder instance + */ + fun userId(userId: String): GenerateKey + + /** + * Set a password for the key. + * + * @param password password to protect the key + * @return builder instance + * @throws UnsupportedOption if key passwords are not supported + * @throws PasswordNotHumanReadable if the password is not human-readable + */ + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) + fun withKeyPassword(password: String): GenerateKey + + /** + * Set a password for the key. + * + * @param password password to protect the key + * @return builder instance + * @throws PasswordNotHumanReadable if the password is not human-readable + * @throws UnsupportedOption if key passwords are not supported + */ + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) + fun withKeyPassword(password: ByteArray): GenerateKey = + try { + withKeyPassword(UTF8Util.decodeUTF8(password)) + } catch (e: CharacterCodingException) { + throw PasswordNotHumanReadable() + } + + /** + * Pass in a profile. + * + * @param profile profile + * @return builder instance + */ + fun profile(profile: Profile): GenerateKey = profile(profile.name) + + /** + * Pass in a profile identifier. + * + * @param profile profile identifier + * @return builder instance + */ + fun profile(profile: String): GenerateKey + + /** + * If this options is set, the generated key will not be capable of encryption / decryption. + * + * @return builder instance + */ + fun signingOnly(): GenerateKey + + /** + * Generate the OpenPGP key and return it encoded as an [InputStream]. + * + * @return key + * @throws MissingArg if no user-id was provided + * @throws UnsupportedAsymmetricAlgo if the generated key uses an unsupported asymmetric + * algorithm + * @throws IOException in case of an IO error + */ + @Throws(MissingArg::class, UnsupportedAsymmetricAlgo::class, IOException::class) + fun generate(): Ready +} From 8df4a520bdb6c2e37ea81cf71f0cf7e8b422eb7e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:20:29 +0100 Subject: [PATCH 087/298] Kotlin conversion: InlineDetach --- .../main/java/sop/operation/InlineDetach.java | 52 ------------------- .../main/kotlin/sop/operation/InlineDetach.kt | 43 +++++++++++++++ 2 files changed, 43 insertions(+), 52 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/InlineDetach.java create mode 100644 sop-java/src/main/kotlin/sop/operation/InlineDetach.kt diff --git a/sop-java/src/main/java/sop/operation/InlineDetach.java b/sop-java/src/main/java/sop/operation/InlineDetach.java deleted file mode 100644 index aba40b1..0000000 --- a/sop-java/src/main/java/sop/operation/InlineDetach.java +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.ReadyWithResult; -import sop.Signatures; -import sop.exception.SOPGPException; - -/** - * Split cleartext signed messages up into data and signatures. - */ -public interface InlineDetach { - - /** - * Do not wrap the signatures in ASCII armor. - * @return builder - */ - InlineDetach noArmor(); - - /** - * Detach the provided signed message from its signatures. - * - * @param messageInputStream input stream containing the signed message - * @return result containing the detached message - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.BadData if the input stream does not contain a signed message - */ - ReadyWithResult message(InputStream messageInputStream) - throws IOException, - SOPGPException.BadData; - - /** - * Detach the provided cleartext signed message from its signatures. - * - * @param message byte array containing the signed message - * @return result containing the detached message - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain a signed message - */ - default ReadyWithResult message(byte[] message) - throws IOException, - SOPGPException.BadData { - return message(new ByteArrayInputStream(message)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt b/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt new file mode 100644 index 0000000..941a9bf --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.ReadyWithResult +import sop.Signatures +import sop.exception.SOPGPException.BadData + +interface InlineDetach { + + /** + * Do not wrap the signatures in ASCII armor. + * + * @return builder + */ + fun noArmor(): InlineDetach + + /** + * Detach the provided signed message from its signatures. + * + * @param messageInputStream input stream containing the signed message + * @return result containing the detached message + * @throws IOException in case of an IO error + * @throws BadData if the input stream does not contain a signed message + */ + @Throws(IOException::class, BadData::class) + fun message(messageInputStream: InputStream): ReadyWithResult + + /** + * Detach the provided cleartext signed message from its signatures. + * + * @param message byte array containing the signed message + * @return result containing the detached message + * @throws IOException in case of an IO error + * @throws BadData if the byte array does not contain a signed message + */ + @Throws(IOException::class, BadData::class) + fun message(message: ByteArray): ReadyWithResult = message(message.inputStream()) +} From 9283f81c56662d63c565dfd2aaac14868c4bbc3d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:22:12 +0100 Subject: [PATCH 088/298] Replace ByteArrayInputStream with inputStream() --- sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt | 4 +--- sop-java/src/main/kotlin/sop/operation/Encrypt.kt | 7 +++---- sop-java/src/main/kotlin/sop/operation/ExtractCert.kt | 3 +-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt b/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt index 2dd9218..d899b54 100644 --- a/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt +++ b/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt @@ -4,7 +4,6 @@ package sop.operation -import java.io.ByteArrayInputStream import java.io.IOException import java.io.InputStream import sop.exception.SOPGPException.BadData @@ -31,6 +30,5 @@ interface DetachedVerify : AbstractVerify, VerifySignatures { * @throws IOException in case of an IO error */ @Throws(BadData::class, IOException::class) - fun signatures(signatures: ByteArray): VerifySignatures = - signatures(ByteArrayInputStream(signatures)) + fun signatures(signatures: ByteArray): VerifySignatures = signatures(signatures.inputStream()) } diff --git a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt index ad30149..0daebee 100644 --- a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt +++ b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt @@ -4,7 +4,6 @@ package sop.operation -import java.io.ByteArrayInputStream import java.io.IOException import java.io.InputStream import sop.Profile @@ -57,7 +56,7 @@ interface Encrypt { */ @Throws( KeyCannotSign::class, UnsupportedAsymmetricAlgo::class, BadData::class, IOException::class) - fun signWith(key: ByteArray): Encrypt = signWith(ByteArrayInputStream(key)) + fun signWith(key: ByteArray): Encrypt = signWith(key.inputStream()) /** * Provide the password for the secret key used for signing. @@ -125,7 +124,7 @@ interface Encrypt { UnsupportedAsymmetricAlgo::class, BadData::class, IOException::class) - fun withCert(cert: ByteArray): Encrypt = withCert(ByteArrayInputStream(cert)) + fun withCert(cert: ByteArray): Encrypt = withCert(cert.inputStream()) /** * Pass in a profile. @@ -162,5 +161,5 @@ interface Encrypt { * @throws KeyIsProtected if at least one signing key cannot be unlocked */ @Throws(IOException::class, KeyIsProtected::class) - fun plaintext(plaintext: ByteArray): Ready = plaintext(ByteArrayInputStream(plaintext)) + fun plaintext(plaintext: ByteArray): Ready = plaintext(plaintext.inputStream()) } diff --git a/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt b/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt index bd48e08..e2ce1cc 100644 --- a/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt +++ b/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt @@ -4,7 +4,6 @@ package sop.operation -import java.io.ByteArrayInputStream import java.io.IOException import java.io.InputStream import sop.Ready @@ -38,5 +37,5 @@ interface ExtractCert { * @throws BadData if the byte array does not contain an OpenPGP key */ @Throws(IOException::class, BadData::class) - fun key(key: ByteArray): Ready = key(ByteArrayInputStream(key)) + fun key(key: ByteArray): Ready = key(key.inputStream()) } From be0ceb0886a9b6a51b51a4519b4098e761ad0cf3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:25:25 +0100 Subject: [PATCH 089/298] Kotlin conversion: InlineSign --- .../main/java/sop/operation/InlineSign.java | 60 ------------------- .../main/kotlin/sop/operation/InlineSign.kt | 47 +++++++++++++++ 2 files changed, 47 insertions(+), 60 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/InlineSign.java create mode 100644 sop-java/src/main/kotlin/sop/operation/InlineSign.kt diff --git a/sop-java/src/main/java/sop/operation/InlineSign.java b/sop-java/src/main/java/sop/operation/InlineSign.java deleted file mode 100644 index d45aebd..0000000 --- a/sop-java/src/main/java/sop/operation/InlineSign.java +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.Ready; -import sop.enums.InlineSignAs; -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -public interface InlineSign extends AbstractSign { - - /** - * Sets the signature mode. - * Note: This method has to be called before {@link #key(InputStream)} is called. - * - * @param mode signature mode - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - InlineSign mode(InlineSignAs mode) - throws SOPGPException.UnsupportedOption; - - /** - * Signs data. - * - * @param data input stream containing data - * @return ready - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - * @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered - */ - Ready data(InputStream data) - throws IOException, - SOPGPException.KeyIsProtected, - SOPGPException.ExpectedText; - - /** - * Signs data. - * - * @param data byte array containing data - * @return ready - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - * @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered - */ - default Ready data(byte[] data) - throws IOException, - SOPGPException.KeyIsProtected, - SOPGPException.ExpectedText { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/InlineSign.kt b/sop-java/src/main/kotlin/sop/operation/InlineSign.kt new file mode 100644 index 0000000..11b5668 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/InlineSign.kt @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.Ready +import sop.enums.InlineSignAs +import sop.exception.SOPGPException.* + +interface InlineSign : AbstractSign { + + /** + * Sets the signature mode. Note: This method has to be called before [.key] is called. + * + * @param mode signature mode + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun mode(mode: InlineSignAs): InlineSign + + /** + * Signs data. + * + * @param data input stream containing data + * @return ready + * @throws IOException in case of an IO error + * @throws KeyIsProtected if at least one signing key cannot be unlocked + * @throws ExpectedText if text data was expected, but binary data was encountered + */ + @Throws(IOException::class, KeyIsProtected::class, ExpectedText::class) + fun data(data: InputStream): Ready + + /** + * Signs data. + * + * @param data byte array containing data + * @return ready + * @throws IOException in case of an IO error + * @throws KeyIsProtected if at least one signing key cannot be unlocked + * @throws ExpectedText if text data was expected, but binary data was encountered + */ + @Throws(IOException::class, KeyIsProtected::class, ExpectedText::class) + fun data(data: ByteArray): Ready = data(data.inputStream()) +} From 6c14f249bb66ba8ca019c8235fe2c21112baf876 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:25:47 +0100 Subject: [PATCH 090/298] Kotlin conversion: InlineVerify --- .../main/java/sop/operation/InlineVerify.java | 54 ------------------- .../main/kotlin/sop/operation/InlineVerify.kt | 42 +++++++++++++++ 2 files changed, 42 insertions(+), 54 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/InlineVerify.java create mode 100644 sop-java/src/main/kotlin/sop/operation/InlineVerify.kt diff --git a/sop-java/src/main/java/sop/operation/InlineVerify.java b/sop-java/src/main/java/sop/operation/InlineVerify.java deleted file mode 100644 index ac662a0..0000000 --- a/sop-java/src/main/java/sop/operation/InlineVerify.java +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.ReadyWithResult; -import sop.Verification; -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -/** - * API for verification of inline-signed messages. - */ -public interface InlineVerify extends AbstractVerify { - - /** - * Provide the inline-signed data. - * The result can be used to write the plaintext message out and to get the verifications. - * - * @param data signed data - * @return list of signature verifications - * - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - ReadyWithResult> data(InputStream data) - throws IOException, - SOPGPException.NoSignature, - SOPGPException.BadData; - - /** - * Provide the inline-signed data. - * The result can be used to write the plaintext message out and to get the verifications. - * - * @param data signed data - * @return list of signature verifications - * - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - default ReadyWithResult> data(byte[] data) - throws IOException, - SOPGPException.NoSignature, - SOPGPException.BadData { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt b/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt new file mode 100644 index 0000000..c16b269 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.ReadyWithResult +import sop.Verification +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.NoSignature + +/** API for verification of inline-signed messages. */ +interface InlineVerify : AbstractVerify { + + /** + * Provide the inline-signed data. The result can be used to write the plaintext message out and + * to get the verifications. + * + * @param data signed data + * @return list of signature verifications + * @throws IOException in case of an IO error + * @throws NoSignature when no signature is found + * @throws BadData when the data is invalid OpenPGP data + */ + @Throws(IOException::class, NoSignature::class, BadData::class) + fun data(data: InputStream): ReadyWithResult> + + /** + * Provide the inline-signed data. The result can be used to write the plaintext message out and + * to get the verifications. + * + * @param data signed data + * @return list of signature verifications + * @throws IOException in case of an IO error + * @throws NoSignature when no signature is found + * @throws BadData when the data is invalid OpenPGP data + */ + @Throws(IOException::class, NoSignature::class, BadData::class) + fun data(data: ByteArray): ReadyWithResult> = data(data.inputStream()) +} From 145cadef4f7557a52529eaea1a04eb3bcb484868 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:27:17 +0100 Subject: [PATCH 091/298] Kotlin conversion: ListProfiles --- .../main/java/sop/operation/ListProfiles.java | 43 ------------------- .../main/kotlin/sop/operation/ListProfiles.kt | 34 +++++++++++++++ 2 files changed, 34 insertions(+), 43 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/ListProfiles.java create mode 100644 sop-java/src/main/kotlin/sop/operation/ListProfiles.kt diff --git a/sop-java/src/main/java/sop/operation/ListProfiles.java b/sop-java/src/main/java/sop/operation/ListProfiles.java deleted file mode 100644 index 0c17bd6..0000000 --- a/sop-java/src/main/java/sop/operation/ListProfiles.java +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.Profile; - -import java.util.List; - -/** - * Subcommand to list supported profiles of other subcommands. - */ -public interface ListProfiles { - - /** - * Provide the name of the subcommand for which profiles shall be listed. - * The returned list of profiles MUST NOT contain more than 4 entries. - * - * @param command command name (e.g.

generate-key
) - * @return list of profiles. - */ - List subcommand(String command); - - /** - * Return a list of {@link Profile Profiles} supported by the {@link GenerateKey} implementation. - * - * @return profiles - */ - default List generateKey() { - return subcommand("generate-key"); - } - - /** - * Return a list of {@link Profile Profiles} supported by the {@link Encrypt} implementation. - * - * @return profiles - */ - default List encrypt() { - return subcommand("encrypt"); - } - -} diff --git a/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt b/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt new file mode 100644 index 0000000..315faf2 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import sop.Profile + +/** Subcommand to list supported profiles of other subcommands. */ +interface ListProfiles { + + /** + * Provide the name of the subcommand for which profiles shall be listed. The returned list of + * profiles MUST NOT contain more than 4 entries. + * + * @param command command name (e.g. `generate-key`) + * @return list of profiles. + */ + fun subcommand(command: String): List + + /** + * Return a list of [Profiles][Profile] supported by the [GenerateKey] implementation. + * + * @return profiles + */ + fun generateKey(): List = subcommand("generate-key") + + /** + * Return a list of [Profiles][Profile] supported by the [Encrypt] implementation. + * + * @return profiles + */ + fun encrypt(): List = subcommand("encrypt") +} From 0ee4638bebf88f28fc434f9c78b6fef45c8a877a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:28:59 +0100 Subject: [PATCH 092/298] Kotlin conversion: RevokeKey --- .../main/java/sop/operation/RevokeKey.java | 54 ------------------- .../main/kotlin/sop/operation/RevokeKey.kt | 48 +++++++++++++++++ 2 files changed, 48 insertions(+), 54 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/RevokeKey.java create mode 100644 sop-java/src/main/kotlin/sop/operation/RevokeKey.kt diff --git a/sop-java/src/main/java/sop/operation/RevokeKey.java b/sop-java/src/main/java/sop/operation/RevokeKey.java deleted file mode 100644 index 3ceb5b3..0000000 --- a/sop-java/src/main/java/sop/operation/RevokeKey.java +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2021 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.ByteArrayInputStream; -import java.io.InputStream; - -public interface RevokeKey { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - RevokeKey noArmor(); - - /** - * Provide the decryption password for the secret key. - * - * @param password password - * @return builder instance - * @throws SOPGPException.UnsupportedOption if the implementation does not support key passwords - * @throws SOPGPException.PasswordNotHumanReadable if the password is not human-readable - */ - default RevokeKey withKeyPassword(String password) - throws SOPGPException.UnsupportedOption, - SOPGPException.PasswordNotHumanReadable { - return withKeyPassword(password.getBytes(UTF8Util.UTF8)); - } - - /** - * Provide the decryption password for the secret key. - * - * @param password password - * @return builder instance - * @throws SOPGPException.UnsupportedOption if the implementation does not support key passwords - * @throws SOPGPException.PasswordNotHumanReadable if the password is not human-readable - */ - RevokeKey withKeyPassword(byte[] password) - throws SOPGPException.UnsupportedOption, - SOPGPException.PasswordNotHumanReadable; - - default Ready keys(byte[] bytes) { - return keys(new ByteArrayInputStream(bytes)); - } - - Ready keys(InputStream keys); -} diff --git a/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt b/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt new file mode 100644 index 0000000..f3cbe5c --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.InputStream +import sop.Ready +import sop.exception.SOPGPException.PasswordNotHumanReadable +import sop.exception.SOPGPException.UnsupportedOption +import sop.util.UTF8Util + +interface RevokeKey { + + /** + * Disable ASCII armor encoding. + * + * @return builder instance + */ + fun noArmor(): RevokeKey + + /** + * Provide the decryption password for the secret key. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if the implementation does not support key passwords + * @throws PasswordNotHumanReadable if the password is not human-readable + */ + @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + fun withKeyPassword(password: String): RevokeKey = + withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + + /** + * Provide the decryption password for the secret key. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if the implementation does not support key passwords + * @throws PasswordNotHumanReadable if the password is not human-readable + */ + @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + fun withKeyPassword(password: ByteArray): RevokeKey + + fun keys(bytes: ByteArray): Ready = keys(bytes.inputStream()) + + fun keys(keys: InputStream): Ready +} From a8c2e72ef589bdeea16df71c2901842b6fdd5220 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:30:24 +0100 Subject: [PATCH 093/298] Kotlin conversion: VerifySignatures --- .../java/sop/operation/VerifySignatures.java | 46 ------------------- .../kotlin/sop/operation/VerifySignatures.kt | 38 +++++++++++++++ 2 files changed, 38 insertions(+), 46 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/VerifySignatures.java create mode 100644 sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt diff --git a/sop-java/src/main/java/sop/operation/VerifySignatures.java b/sop-java/src/main/java/sop/operation/VerifySignatures.java deleted file mode 100644 index 5181514..0000000 --- a/sop-java/src/main/java/sop/operation/VerifySignatures.java +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -import sop.Verification; -import sop.exception.SOPGPException; - -public interface VerifySignatures { - - /** - * Provide the signed data (without signatures). - * - * @param data signed data - * @return list of signature verifications - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no valid signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - List data(InputStream data) - throws IOException, - SOPGPException.NoSignature, - SOPGPException.BadData; - - /** - * Provide the signed data (without signatures). - * - * @param data signed data - * @return list of signature verifications - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no valid signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - default List data(byte[] data) - throws IOException, - SOPGPException.NoSignature, - SOPGPException.BadData { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt b/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt new file mode 100644 index 0000000..b75e4a5 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.Verification +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.NoSignature + +interface VerifySignatures { + + /** + * Provide the signed data (without signatures). + * + * @param data signed data + * @return list of signature verifications + * @throws IOException in case of an IO error + * @throws NoSignature when no valid signature is found + * @throws BadData when the data is invalid OpenPGP data + */ + @Throws(IOException::class, NoSignature::class, BadData::class) + fun data(data: InputStream): List + + /** + * Provide the signed data (without signatures). + * + * @param data signed data + * @return list of signature verifications + * @throws IOException in case of an IO error + * @throws NoSignature when no valid signature is found + * @throws BadData when the data is invalid OpenPGP data + */ + @Throws(IOException::class, NoSignature::class, BadData::class) + fun data(data: ByteArray): List = data(data.inputStream()) +} From d0ee9c2066ce0d72cbbd5fb9bcbdf09fbaaa3f17 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:38:34 +0100 Subject: [PATCH 094/298] Kotlin conversion: Version --- .../src/main/java/sop/operation/Version.java | 109 ------------------ .../main/java/sop/operation/package-info.java | 9 -- .../src/main/kotlin/sop/operation/Version.kt | 100 ++++++++++++++++ 3 files changed, 100 insertions(+), 118 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/Version.java delete mode 100644 sop-java/src/main/java/sop/operation/package-info.java create mode 100644 sop-java/src/main/kotlin/sop/operation/Version.kt diff --git a/sop-java/src/main/java/sop/operation/Version.java b/sop-java/src/main/java/sop/operation/Version.java deleted file mode 100644 index b6d66b9..0000000 --- a/sop-java/src/main/java/sop/operation/Version.java +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -public interface Version { - - /** - * Return the implementations name. - * e.g. "SOP", - * - * @return implementation name - */ - String getName(); - - /** - * Return the implementations short version string. - * e.g. "1.0" - * - * @return version string - */ - String getVersion(); - - /** - * Return version information about the used OpenPGP backend. - * e.g. "Bouncycastle 1.70" - * - * @return backend version string - */ - String getBackendVersion(); - - /** - * Return an extended version string containing multiple lines of version information. - * The first line MUST match the information produced by {@link #getName()} and {@link #getVersion()}, but the rest of the text - * has no defined structure. - * Example: - *
-     *     "SOP 1.0
-     *     Awesome PGP!
-     *     Using Bouncycastle 1.70
-     *     LibFoo 1.2.2
-     *     See https://pgp.example.org/sop/ for more information"
-     * 
- * - * @return extended version string - */ - String getExtendedVersion(); - - /** - * Return the revision of the SOP specification that this implementation is implementing, for example, - *
draft-dkg-openpgp-stateless-cli-06
. - * If the implementation targets a specific draft but the implementer knows the implementation is incomplete, - * it should prefix the draft title with a "~" (TILDE, U+007E), for example: - *
~draft-dkg-openpgp-stateless-cli-06
. - * The implementation MAY emit additional text about its relationship to the targeted draft on the lines following - * the versioned title. - * - * @return implemented SOP spec version - */ - default String getSopSpecVersion() { - StringBuilder sb = new StringBuilder(); - if (isSopSpecImplementationIncomplete()) { - sb.append('~'); - } - - sb.append(getSopSpecRevisionName()); - - if (getSopSpecImplementationRemarks() != null) { - sb.append('\n') - .append('\n') - .append(getSopSpecImplementationRemarks()); - } - - return sb.toString(); - } - - /** - * Return the version number of the latest targeted SOP spec revision. - * - * @return SOP spec revision number - */ - int getSopSpecRevisionNumber(); - - /** - * Return the name of the latest targeted revision of the SOP spec. - * - * @return SOP spec revision string - */ - default String getSopSpecRevisionName() { - return "draft-dkg-openpgp-stateless-cli-" + String.format("%02d", getSopSpecRevisionNumber()); - } - - /** - * Return
true
, if this implementation of the SOP spec is known to be incomplete or defective. - * - * @return true if incomplete, false otherwise - */ - boolean isSopSpecImplementationIncomplete(); - - /** - * Return free-form text containing remarks about the completeness of the SOP implementation. - * If there are no remarks, this method returns
null
. - * - * @return remarks or null - */ - String getSopSpecImplementationRemarks(); - -} diff --git a/sop-java/src/main/java/sop/operation/package-info.java b/sop-java/src/main/java/sop/operation/package-info.java deleted file mode 100644 index dde4d5b..0000000 --- a/sop-java/src/main/java/sop/operation/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - * Different cryptographic operations. - */ -package sop.operation; diff --git a/sop-java/src/main/kotlin/sop/operation/Version.kt b/sop-java/src/main/kotlin/sop/operation/Version.kt new file mode 100644 index 0000000..9b3bd8a --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/Version.kt @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +interface Version { + + /** + * Return the implementations name. e.g. `SOP`, + * + * @return implementation name + */ + fun getName(): String + + /** + * Return the implementations short version string. e.g. `1.0` + * + * @return version string + */ + fun getVersion(): String + + /** + * Return version information about the used OpenPGP backend. e.g. `Bouncycastle 1.70` + * + * @return backend version string + */ + fun getBackendVersion(): String + + /** + * Return an extended version string containing multiple lines of version information. The first + * line MUST match the information produced by [getName] and [getVersion], but the rest of the + * text has no defined structure. Example: + * ``` + * "SOP 1.0 + * Awesome PGP! + * Using Bouncycastle 1.70 + * LibFoo 1.2.2 + * See https://pgp.example.org/sop/ for more information" + * ``` + * + * @return extended version string + */ + fun getExtendedVersion(): String + + /** + * Return the revision of the SOP specification that this implementation is implementing, for + * example, `draft-dkg-openpgp-stateless-cli-06`. If the implementation targets a specific draft + * but the implementer knows the implementation is incomplete, it should prefix the draft title + * with a `~` (TILDE, U+007E), for example: `~draft-dkg-openpgp-stateless-cli-06`. The + * implementation MAY emit additional text about its relationship to the targeted draft on the + * lines following the versioned title. + * + * @return implemented SOP spec version + */ + fun getSopSpecVersion(): String { + return buildString { + if (isSopSpecImplementationIncomplete()) append('~') + append(getSopSpecRevisionName()) + if (getSopSpecImplementationRemarks() != null) { + append('\n') + append('\n') + append(getSopSpecImplementationRemarks()) + } + } + } + + /** + * Return the version number of the latest targeted SOP spec revision. + * + * @return SOP spec revision number + */ + fun getSopSpecRevisionNumber(): Int + + /** + * Return the name of the latest targeted revision of the SOP spec. + * + * @return SOP spec revision string + */ + fun getSopSpecRevisionName(): String = buildString { + append("draft-dkg-openpgp-stateless-cli-") + append(String.format("%02d", getSopSpecRevisionNumber())) + } + + /** + * Return
true
, if this implementation of the SOP spec is known to be incomplete or + * defective. + * + * @return true if incomplete, false otherwise + */ + fun isSopSpecImplementationIncomplete(): Boolean + + /** + * Return free-form text containing remarks about the completeness of the SOP implementation. If + * there are no remarks, this method returns
null
. + * + * @return remarks or null + */ + fun getSopSpecImplementationRemarks(): String? +} From 049c18c17ba3907057c65f39dbfa5bf57a0f2960 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:45:56 +0100 Subject: [PATCH 095/298] Fix sop-java-picocli jar task --- sop-java-picocli/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/sop-java-picocli/build.gradle b/sop-java-picocli/build.gradle index 664c385..438ef50 100644 --- a/sop-java-picocli/build.gradle +++ b/sop-java-picocli/build.gradle @@ -38,6 +38,7 @@ application { jar { dependsOn(":sop-java:jar") + duplicatesStrategy(DuplicatesStrategy.EXCLUDE) manifest { attributes 'Main-Class': "$mainClassName" From 4b9e2c206f18bcec162bb3a82674c151a61239fc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:46:41 +0100 Subject: [PATCH 096/298] Fix DecryptionResult constructor --- sop-java/src/main/kotlin/sop/DecryptionResult.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/DecryptionResult.kt b/sop-java/src/main/kotlin/sop/DecryptionResult.kt index 1ba98bb..b653297 100644 --- a/sop-java/src/main/kotlin/sop/DecryptionResult.kt +++ b/sop-java/src/main/kotlin/sop/DecryptionResult.kt @@ -6,11 +6,10 @@ package sop import sop.util.Optional -data class DecryptionResult -internal constructor(val sessionKey: Optional, val verifications: List) { +class DecryptionResult(sessionKey: SessionKey?, val verifications: List) { + val sessionKey: Optional - constructor( - sessionKey: SessionKey?, - verifications: List - ) : this(Optional.ofNullable(sessionKey), verifications) + init { + this.sessionKey = Optional.ofNullable(sessionKey) + } } From d5c0d4e390ccd0b0c0171c621eed497d8c897a3b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:47:14 +0100 Subject: [PATCH 097/298] Kotlin conversion: ArmorLabel --- .../src/main/java/sop/enums/ArmorLabel.java | 19 ------------------- .../src/main/kotlin/sop/enums/ArmorLabel.kt | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 19 deletions(-) delete mode 100644 sop-java/src/main/java/sop/enums/ArmorLabel.java create mode 100644 sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt diff --git a/sop-java/src/main/java/sop/enums/ArmorLabel.java b/sop-java/src/main/java/sop/enums/ArmorLabel.java deleted file mode 100644 index bb97e84..0000000 --- a/sop-java/src/main/java/sop/enums/ArmorLabel.java +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum ArmorLabel { - Auto, - Sig, - Key, - Cert, - Message, - ; - - @Override - public String toString() { - return super.toString().toLowerCase(); - } -} diff --git a/sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt b/sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt new file mode 100644 index 0000000..8b4e2cd --- /dev/null +++ b/sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt @@ -0,0 +1,14 @@ +// 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 +} From 1c290e0c8f4cae89108adcae5d79334513c49822 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:48:48 +0100 Subject: [PATCH 098/298] Kotlin conversion: EncryptAs --- sop-java/src/main/java/sop/enums/EncryptAs.java | 16 ---------------- sop-java/src/main/kotlin/sop/enums/EncryptAs.kt | 10 ++++++++++ .../testsuite/operation/EncryptDecryptTest.java | 4 ++-- 3 files changed, 12 insertions(+), 18 deletions(-) delete mode 100644 sop-java/src/main/java/sop/enums/EncryptAs.java create mode 100644 sop-java/src/main/kotlin/sop/enums/EncryptAs.kt diff --git a/sop-java/src/main/java/sop/enums/EncryptAs.java b/sop-java/src/main/java/sop/enums/EncryptAs.java deleted file mode 100644 index 7e7d4d1..0000000 --- a/sop-java/src/main/java/sop/enums/EncryptAs.java +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum EncryptAs { - Binary, - Text, - ; - - @Override - public String toString() { - return super.toString().toLowerCase(); - } -} diff --git a/sop-java/src/main/kotlin/sop/enums/EncryptAs.kt b/sop-java/src/main/kotlin/sop/enums/EncryptAs.kt new file mode 100644 index 0000000..0b6aa8e --- /dev/null +++ b/sop-java/src/main/kotlin/sop/enums/EncryptAs.kt @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.enums + +enum class EncryptAs { + binary, + text +} 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 0c382dc..9138f64 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java @@ -142,7 +142,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] ciphertext = sop.encrypt() .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) - .mode(EncryptAs.Binary) + .mode(EncryptAs.binary) .plaintext(message) .getBytes(); @@ -173,7 +173,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] ciphertext = sop.encrypt() .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) - .mode(EncryptAs.Text) + .mode(EncryptAs.text) .plaintext(message) .getBytes(); From be6be3deac79a922f24285d45b024438b22b3b04 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:50:09 +0100 Subject: [PATCH 099/298] Kotlin conversion: InlineSignAs --- .../src/main/java/sop/enums/InlineSignAs.java | 24 ------------------- .../src/main/kotlin/sop/enums/InlineSignAs.kt | 17 +++++++++++++ 2 files changed, 17 insertions(+), 24 deletions(-) delete mode 100644 sop-java/src/main/java/sop/enums/InlineSignAs.java create mode 100644 sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt diff --git a/sop-java/src/main/java/sop/enums/InlineSignAs.java b/sop-java/src/main/java/sop/enums/InlineSignAs.java deleted file mode 100644 index c1097df..0000000 --- a/sop-java/src/main/java/sop/enums/InlineSignAs.java +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum InlineSignAs { - - /** - * Signature is made over the binary message. - */ - binary, - - /** - * Signature is made over the message in text mode. - */ - text, - - /** - * Signature is made using the Cleartext Signature Framework. - */ - clearsigned, -} - diff --git a/sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt b/sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt new file mode 100644 index 0000000..3440edf --- /dev/null +++ b/sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.enums + +enum class InlineSignAs { + + /** Signature is made over the binary message. */ + binary, + + /** Signature is made over the message in text mode. */ + text, + + /** Signature is made using the Cleartext Signature Framework. */ + clearsigned +} From 30c369d24a10b50b64064963b643e0cd203efec9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:51:16 +0100 Subject: [PATCH 100/298] Kotlin conversion: SignAs --- sop-java/src/main/java/sop/enums/SignAs.java | 23 ------------------- sop-java/src/main/kotlin/sop/enums/SignAs.kt | 12 ++++++++++ .../DetachedSignDetachedVerifyTest.java | 2 +- 3 files changed, 13 insertions(+), 24 deletions(-) delete mode 100644 sop-java/src/main/java/sop/enums/SignAs.java create mode 100644 sop-java/src/main/kotlin/sop/enums/SignAs.kt diff --git a/sop-java/src/main/java/sop/enums/SignAs.java b/sop-java/src/main/java/sop/enums/SignAs.java deleted file mode 100644 index 1174098..0000000 --- a/sop-java/src/main/java/sop/enums/SignAs.java +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum SignAs { - /** - * Signature is made over the binary message. - */ - Binary, - - /** - * Signature is made over the message in text mode. - */ - Text, - ; - - @Override - public String toString() { - return super.toString().toLowerCase(); - } -} diff --git a/sop-java/src/main/kotlin/sop/enums/SignAs.kt b/sop-java/src/main/kotlin/sop/enums/SignAs.kt new file mode 100644 index 0000000..832e831 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/enums/SignAs.kt @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.enums + +enum class SignAs { + /** Signature is made over the binary message. */ + binary, + /** Signature is made over the message in text mode. */ + text +} diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java index e715c14..e404599 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java @@ -62,7 +62,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { byte[] signature = sop.detachedSign() .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) - .mode(SignAs.Text) + .mode(SignAs.text) .data(message) .toByteArrayAndResult() .getBytes(); From 01f98df80b4f5e6757b887fe99b3c1efc21ceb10 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:52:45 +0100 Subject: [PATCH 101/298] Kotlin conversion: SignatureMode --- .../main/java/sop/enums/SignatureMode.java | 25 ------------------- .../src/main/java/sop/enums/package-info.java | 9 ------- .../main/kotlin/sop/enums/SignatureMode.kt | 18 +++++++++++++ 3 files changed, 18 insertions(+), 34 deletions(-) delete mode 100644 sop-java/src/main/java/sop/enums/SignatureMode.java delete mode 100644 sop-java/src/main/java/sop/enums/package-info.java create mode 100644 sop-java/src/main/kotlin/sop/enums/SignatureMode.kt diff --git a/sop-java/src/main/java/sop/enums/SignatureMode.java b/sop-java/src/main/java/sop/enums/SignatureMode.java deleted file mode 100644 index 71ce7d8..0000000 --- a/sop-java/src/main/java/sop/enums/SignatureMode.java +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -/** - * Enum referencing relevant signature types. - * - * @see - * RFC4880 §5.2.1 - Signature Types - */ -public enum SignatureMode { - /** - * Signature of a binary document (
0x00
). - */ - binary, - - /** - * Signature of a canonical text document (
0x01
). - */ - text - - // Other Signature Types are irrelevant. -} diff --git a/sop-java/src/main/java/sop/enums/package-info.java b/sop-java/src/main/java/sop/enums/package-info.java deleted file mode 100644 index 67148d3..0000000 --- a/sop-java/src/main/java/sop/enums/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - * Enumerations. - */ -package sop.enums; diff --git a/sop-java/src/main/kotlin/sop/enums/SignatureMode.kt b/sop-java/src/main/kotlin/sop/enums/SignatureMode.kt new file mode 100644 index 0000000..7213195 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/enums/SignatureMode.kt @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.enums + +/** + * Enum referencing relevant signature types. + * + * @see RFC4880 §5.2.1 - Signature + * Types + */ +enum class SignatureMode { + /** Signature of a binary document (type `0x00`). */ + binary, + /** Signature of a canonical text document (type `0x01`). */ + text +} From b7007cc0070659c58e0e9cad520acd58b6b07342 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 15:00:21 +0100 Subject: [PATCH 102/298] Kotlin conversion: HexUtil --- sop-java/src/main/java/sop/util/HexUtil.java | 47 -------------------- sop-java/src/main/kotlin/sop/util/HexUtil.kt | 38 ++++++++++++++++ 2 files changed, 38 insertions(+), 47 deletions(-) delete mode 100644 sop-java/src/main/java/sop/util/HexUtil.java create mode 100644 sop-java/src/main/kotlin/sop/util/HexUtil.kt diff --git a/sop-java/src/main/java/sop/util/HexUtil.java b/sop-java/src/main/java/sop/util/HexUtil.java deleted file mode 100644 index 9b88f53..0000000 --- a/sop-java/src/main/java/sop/util/HexUtil.java +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2021 Paul Schaub, @maybeWeCouldStealAVan, @Dave L. -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -public class HexUtil { - - private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); - - /** - * Encode a byte array to a hex string. - * - * @see - * How to convert a byte array to a hex string in Java? - * @param bytes bytes - * @return hex encoding - */ - public static String bytesToHex(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = HEX_ARRAY[v >>> 4]; - hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; - } - return new String(hexChars); - } - - /** - * Decode a hex string into a byte array. - * - * @see - * Convert a string representation of a hex dump to a byte array using Java? - * @param s hex string - * @return decoded byte array - */ - public static byte[] hexToBytes(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i + 1), 16)); - } - return data; - } -} diff --git a/sop-java/src/main/kotlin/sop/util/HexUtil.kt b/sop-java/src/main/kotlin/sop/util/HexUtil.kt new file mode 100644 index 0000000..f372137 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/util/HexUtil.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util + +class HexUtil { + + companion object { + /** + * Encode a byte array to a hex string. + * + * @param bytes bytes + * @return hex encoding + * @see + * [Convert Byte Arrays to Hex Strings in Kotlin](https://www.baeldung.com/kotlin/byte-arrays-to-hex-strings) + */ + @JvmStatic fun bytesToHex(bytes: ByteArray): String = bytes.toHex() + + /** + * Decode a hex string into a byte array. + * + * @param s hex string + * @return decoded byte array + * @see + * [Kotlin convert hex string to ByteArray](https://stackoverflow.com/a/66614516/11150851) + */ + @JvmStatic fun hexToBytes(s: String): ByteArray = s.decodeHex() + } +} + +fun String.decodeHex(): ByteArray { + check(length % 2 == 0) { "Hex encoding must have even number of digits." } + + return chunked(2).map { it.toInt(16).toByte() }.toByteArray() +} + +fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02X".format(eachByte) } From 05886228df497c18ce94ffd756fc2075e48b0a95 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 15:07:13 +0100 Subject: [PATCH 103/298] Kotlin conversion: ProxyOutputStream --- .../main/java/sop/util/ProxyOutputStream.java | 80 ------------------- .../main/kotlin/sop/util/ProxyOutputStream.kt | 75 +++++++++++++++++ 2 files changed, 75 insertions(+), 80 deletions(-) delete mode 100644 sop-java/src/main/java/sop/util/ProxyOutputStream.java create mode 100644 sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt diff --git a/sop-java/src/main/java/sop/util/ProxyOutputStream.java b/sop-java/src/main/java/sop/util/ProxyOutputStream.java deleted file mode 100644 index ed24fc2..0000000 --- a/sop-java/src/main/java/sop/util/ProxyOutputStream.java +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -/** - * {@link OutputStream} that buffers data being written into it, until its underlying output stream is being replaced. - * At that point, first all the buffered data is being written to the underlying stream, followed by any successive - * data that may get written to the {@link ProxyOutputStream}. - *

- * This class is useful if we need to provide an {@link OutputStream} at one point in time when the final - * target output stream is not yet known. - */ -public class ProxyOutputStream extends OutputStream { - - private final ByteArrayOutputStream buffer; - private OutputStream swapped; - - public ProxyOutputStream() { - this.buffer = new ByteArrayOutputStream(); - } - - public synchronized void replaceOutputStream(OutputStream underlying) throws IOException { - if (underlying == null) { - throw new NullPointerException("Underlying OutputStream cannot be null."); - } - this.swapped = underlying; - - byte[] bufferBytes = buffer.toByteArray(); - swapped.write(bufferBytes); - } - - @Override - public synchronized void write(byte[] b) throws IOException { - if (swapped == null) { - buffer.write(b); - } else { - swapped.write(b); - } - } - - @Override - public synchronized void write(byte[] b, int off, int len) throws IOException { - if (swapped == null) { - buffer.write(b, off, len); - } else { - swapped.write(b, off, len); - } - } - - @Override - public synchronized void flush() throws IOException { - buffer.flush(); - if (swapped != null) { - swapped.flush(); - } - } - - @Override - public synchronized void close() throws IOException { - buffer.close(); - if (swapped != null) { - swapped.close(); - } - } - - @Override - public synchronized void write(int i) throws IOException { - if (swapped == null) { - buffer.write(i); - } else { - swapped.write(i); - } - } -} diff --git a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt new file mode 100644 index 0000000..142f7b3 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util + +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.OutputStream + +/** + * [OutputStream] that buffers data being written into it, until its underlying output stream is + * being replaced. At that point, first all the buffered data is being written to the underlying + * stream, followed by any successive data that may get written to the [ProxyOutputStream]. This + * 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. + */ +class ProxyOutputStream { + private val buffer = ByteArrayOutputStream() + private var swapped: OutputStream? = null + + @Synchronized + fun replaceOutputStream(underlying: OutputStream) { + this.swapped = underlying + swapped!!.write(buffer.toByteArray()) + } + + @Synchronized + @Throws(IOException::class) + fun write(b: ByteArray) { + if (swapped == null) { + buffer.write(b) + } else { + swapped!!.write(b) + } + } + + @Synchronized + @Throws(IOException::class) + fun write(b: ByteArray, off: Int, len: Int) { + if (swapped == null) { + buffer.write(b, off, len) + } else { + swapped!!.write(b, off, len) + } + } + + @Synchronized + @Throws(IOException::class) + fun flush() { + buffer.flush() + if (swapped != null) { + swapped!!.flush() + } + } + + @Synchronized + @Throws(IOException::class) + fun close() { + buffer.close() + if (swapped != null) { + swapped!!.close() + } + } + + @Synchronized + @Throws(IOException::class) + fun write(i: Int) { + if (swapped == null) { + buffer.write(i) + } else { + swapped!!.write(i) + } + } +} From 25a33611fdab6eba7cb3625677d37ccd5c16fd7d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 15:10:46 +0100 Subject: [PATCH 104/298] Kotlin conversion: UTCUtil --- sop-java/src/main/java/sop/util/UTCUtil.java | 65 -------------------- sop-java/src/main/kotlin/sop/util/UTCUtil.kt | 62 +++++++++++++++++++ 2 files changed, 62 insertions(+), 65 deletions(-) delete mode 100644 sop-java/src/main/java/sop/util/UTCUtil.java create mode 100644 sop-java/src/main/kotlin/sop/util/UTCUtil.kt diff --git a/sop-java/src/main/java/sop/util/UTCUtil.java b/sop-java/src/main/java/sop/util/UTCUtil.java deleted file mode 100644 index 96a6b9c..0000000 --- a/sop-java/src/main/java/sop/util/UTCUtil.java +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import javax.annotation.Nonnull; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.TimeZone; - -/** - * Utility class to parse and format dates as ISO-8601 UTC timestamps. - */ -public class UTCUtil { - - public static final SimpleDateFormat UTC_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - public static final SimpleDateFormat[] UTC_PARSERS = new SimpleDateFormat[] { - UTC_FORMATTER, - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"), - new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'") - }; - - static { - for (SimpleDateFormat f : UTC_PARSERS) { - f.setTimeZone(TimeZone.getTimeZone("UTC")); - } - } - /** - * Parse an ISO-8601 UTC timestamp from a string. - * - * @param dateString string - * @return date - * @throws ParseException if the date string is malformed and cannot be parsed - */ - @Nonnull - public static Date parseUTCDate(String dateString) throws ParseException { - ParseException exception = null; - for (SimpleDateFormat parser : UTC_PARSERS) { - try { - return parser.parse(dateString); - } catch (ParseException e) { - // Store first exception (that of UTC_FORMATTER) to throw if we fail to parse the date - if (exception == null) { - exception = e; - } - // Try next parser - } - } - // No parser worked, so we throw the store exception - throw exception; - } - - /** - * Format a date as ISO-8601 UTC timestamp. - * - * @param date date - * @return timestamp string - */ - public static String formatUTCDate(Date date) { - return UTC_FORMATTER.format(date); - } -} diff --git a/sop-java/src/main/kotlin/sop/util/UTCUtil.kt b/sop-java/src/main/kotlin/sop/util/UTCUtil.kt new file mode 100644 index 0000000..8924f25 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/util/UTCUtil.kt @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util + +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + +class UTCUtil { + + companion object { + + @JvmStatic val UTC_FORMATTER = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") + @JvmStatic + val UTC_PARSERS = + arrayOf( + UTC_FORMATTER, + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"), + SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"), + SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")) + .onEach { fmt -> fmt.timeZone = TimeZone.getTimeZone("UTC") } + + /** + * Parse an ISO-8601 UTC timestamp from a string. + * + * @param dateString string + * @return date + * @throws ParseException if the date string is malformed and cannot be parsed + */ + @JvmStatic + @Throws(ParseException::class) + fun parseUTCDate(dateString: String): Date { + var exception: ParseException? = null + for (parser in UTC_PARSERS) { + try { + return parser.parse(dateString) + } catch (e: ParseException) { + // Store first exception (that of UTC_FORMATTER) to throw if we fail to parse + // the date + if (exception == null) { + exception = e + } + // Try next parser + } + } + throw exception!! + } + + /** + * Format a date as ISO-8601 UTC timestamp. + * + * @param date date + * @return timestamp string + */ + @JvmStatic + fun formatUTCDate(date: Date): String { + return UTC_FORMATTER.format(date) + } + } +} From e1a6ffd07a7e081e207f84d105344013f9535b17 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 15:18:39 +0100 Subject: [PATCH 105/298] Use @JvmField annotation --- sop-java/src/main/kotlin/sop/util/UTCUtil.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/util/UTCUtil.kt b/sop-java/src/main/kotlin/sop/util/UTCUtil.kt index 8924f25..4ae13bc 100644 --- a/sop-java/src/main/kotlin/sop/util/UTCUtil.kt +++ b/sop-java/src/main/kotlin/sop/util/UTCUtil.kt @@ -12,8 +12,8 @@ class UTCUtil { companion object { - @JvmStatic val UTC_FORMATTER = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") - @JvmStatic + @JvmField val UTC_FORMATTER = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") + @JvmField val UTC_PARSERS = arrayOf( UTC_FORMATTER, From 94b428ef62cc9735fad20cca4560eb14af6a2b41 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 15:18:48 +0100 Subject: [PATCH 106/298] Kotlin conversion: UTF8Util --- sop-java/src/main/java/sop/util/UTF8Util.java | 37 ------------------- .../src/main/java/sop/util/package-info.java | 8 ---- sop-java/src/main/kotlin/sop/util/UTF8Util.kt | 37 +++++++++++++++++++ 3 files changed, 37 insertions(+), 45 deletions(-) delete mode 100644 sop-java/src/main/java/sop/util/UTF8Util.java delete mode 100644 sop-java/src/main/java/sop/util/package-info.java create mode 100644 sop-java/src/main/kotlin/sop/util/UTF8Util.kt diff --git a/sop-java/src/main/java/sop/util/UTF8Util.java b/sop-java/src/main/java/sop/util/UTF8Util.java deleted file mode 100644 index 1b4941b..0000000 --- a/sop-java/src/main/java/sop/util/UTF8Util.java +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharacterCodingException; -import java.nio.charset.Charset; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CodingErrorAction; - -public class UTF8Util { - - public static final Charset UTF8 = Charset.forName("UTF8"); - private static final CharsetDecoder UTF8Decoder = UTF8 - .newDecoder() - .onUnmappableCharacter(CodingErrorAction.REPORT) - .onMalformedInput(CodingErrorAction.REPORT); - - /** - * Detect non-valid UTF8 data. - * - * @see ante on StackOverflow - * @param data utf-8 encoded bytes - * - * @return decoded string - * @throws CharacterCodingException if the input data does not resemble UTF8 - */ - public static String decodeUTF8(byte[] data) - throws CharacterCodingException { - ByteBuffer byteBuffer = ByteBuffer.wrap(data); - CharBuffer charBuffer = UTF8Decoder.decode(byteBuffer); - return charBuffer.toString(); - } -} diff --git a/sop-java/src/main/java/sop/util/package-info.java b/sop-java/src/main/java/sop/util/package-info.java deleted file mode 100644 index 3dd9fc1..0000000 --- a/sop-java/src/main/java/sop/util/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Utility classes. - */ -package sop.util; diff --git a/sop-java/src/main/kotlin/sop/util/UTF8Util.kt b/sop-java/src/main/kotlin/sop/util/UTF8Util.kt new file mode 100644 index 0000000..770f32c --- /dev/null +++ b/sop-java/src/main/kotlin/sop/util/UTF8Util.kt @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util + +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.nio.charset.CodingErrorAction + +class UTF8Util { + companion object { + @JvmField val UTF8: Charset = Charset.forName("UTF8") + + @JvmStatic + private val UTF8Decoder = + UTF8.newDecoder() + .onUnmappableCharacter(CodingErrorAction.REPORT) + .onMalformedInput(CodingErrorAction.REPORT) + + /** + * Detect non-valid UTF8 data. + * + * @param data utf-8 encoded bytes + * @return decoded string + * @throws CharacterCodingException if the input data does not resemble UTF8 + * @see [ante on StackOverflow](https://stackoverflow.com/a/1471193) + */ + @JvmStatic + @Throws(CharacterCodingException::class) + fun decodeUTF8(data: ByteArray): String { + val byteBuffer = ByteBuffer.wrap(data) + val charBuffer = UTF8Decoder.decode(byteBuffer) + return charBuffer.toString() + } + } +} From 7824ee92c536f990de01c63d07c2dbfd68f4f579 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 15:27:47 +0100 Subject: [PATCH 107/298] Kotlin conversion: SOPGPException --- .../java/sop/exception/SOPGPException.java | 473 ------------------ .../main/java/sop/exception/package-info.java | 9 - sop-java/src/main/java/sop/package-info.java | 8 - .../kotlin/sop/exception/SOPGPException.kt | 308 ++++++++++++ 4 files changed, 308 insertions(+), 490 deletions(-) delete mode 100644 sop-java/src/main/java/sop/exception/SOPGPException.java delete mode 100644 sop-java/src/main/java/sop/exception/package-info.java delete mode 100644 sop-java/src/main/java/sop/package-info.java create mode 100644 sop-java/src/main/kotlin/sop/exception/SOPGPException.kt diff --git a/sop-java/src/main/java/sop/exception/SOPGPException.java b/sop-java/src/main/java/sop/exception/SOPGPException.java deleted file mode 100644 index 1d13065..0000000 --- a/sop-java/src/main/java/sop/exception/SOPGPException.java +++ /dev/null @@ -1,473 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.exception; - -public abstract class SOPGPException extends RuntimeException { - - public SOPGPException() { - super(); - } - - public SOPGPException(String message) { - super(message); - } - - public SOPGPException(Throwable e) { - super(e); - } - - public SOPGPException(String message, Throwable cause) { - super(message, cause); - } - - public abstract int getExitCode(); - - /** - * No acceptable signatures found (sop verify, inline-verify). - */ - public static class NoSignature extends SOPGPException { - - public static final int EXIT_CODE = 3; - - public NoSignature() { - this("No verifiable signature found."); - } - - public NoSignature(String message) { - super(message); - } - - public NoSignature(String errorMsg, NoSignature e) { - super(errorMsg, e); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Asymmetric algorithm unsupported (sop encrypt, sign, inline-sign). - */ - public static class UnsupportedAsymmetricAlgo extends SOPGPException { - - public static final int EXIT_CODE = 13; - - public UnsupportedAsymmetricAlgo(String message, Throwable e) { - super(message, e); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Certificate not encryption capable (e,g, expired, revoked, unacceptable usage). - */ - public static class CertCannotEncrypt extends SOPGPException { - public static final int EXIT_CODE = 17; - - public CertCannotEncrypt(String message, Throwable cause) { - super(message, cause); - } - - public CertCannotEncrypt(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Missing required argument. - */ - public static class MissingArg extends SOPGPException { - - public static final int EXIT_CODE = 19; - - public MissingArg() { - - } - - public MissingArg(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Incomplete verification instructions (sop decrypt). - */ - public static class IncompleteVerification extends SOPGPException { - - public static final int EXIT_CODE = 23; - - public IncompleteVerification(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Unable to decrypt (sop decrypt). - */ - public static class CannotDecrypt extends SOPGPException { - - public static final int EXIT_CODE = 29; - - public CannotDecrypt() { - - } - - public CannotDecrypt(String errorMsg, Throwable e) { - super(errorMsg, e); - } - - public CannotDecrypt(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Non-UTF-8 or otherwise unreliable password (sop encrypt). - */ - public static class PasswordNotHumanReadable extends SOPGPException { - - public static final int EXIT_CODE = 31; - - public PasswordNotHumanReadable() { - super(); - } - - public PasswordNotHumanReadable(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Unsupported option. - */ - public static class UnsupportedOption extends SOPGPException { - - public static final int EXIT_CODE = 37; - - public UnsupportedOption(String message) { - super(message); - } - - public UnsupportedOption(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Invalid data type (no secret key where KEYS expected, etc.). - */ - public static class BadData extends SOPGPException { - - public static final int EXIT_CODE = 41; - - public BadData(String message) { - super(message); - } - - public BadData(Throwable throwable) { - super(throwable); - } - - public BadData(String message, Throwable throwable) { - super(message, throwable); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Non-Text input where text expected. - */ - public static class ExpectedText extends SOPGPException { - - public static final int EXIT_CODE = 53; - - public ExpectedText() { - super(); - } - - public ExpectedText(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Output file already exists. - */ - public static class OutputExists extends SOPGPException { - - public static final int EXIT_CODE = 59; - - public OutputExists(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Input file does not exist. - */ - public static class MissingInput extends SOPGPException { - - public static final int EXIT_CODE = 61; - - public MissingInput(String message, Throwable cause) { - super(message, cause); - } - - public MissingInput(String errorMsg) { - super(errorMsg); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * A KEYS input is protected (locked) with a password and sop failed to unlock it. - */ - public static class KeyIsProtected extends SOPGPException { - - public static final int EXIT_CODE = 67; - - public KeyIsProtected() { - super(); - } - - public KeyIsProtected(String message) { - super(message); - } - - public KeyIsProtected(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Unsupported subcommand. - */ - public static class UnsupportedSubcommand extends SOPGPException { - - public static final int EXIT_CODE = 69; - - public UnsupportedSubcommand(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * An indirect parameter is a special designator (it starts with @), but sop does not know how to handle the prefix. - */ - public static class UnsupportedSpecialPrefix extends SOPGPException { - - public static final int EXIT_CODE = 71; - - public UnsupportedSpecialPrefix(String errorMsg) { - super(errorMsg); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Exception that gets thrown if a special designator (starting with @) is given, but the filesystem contains - * a file matching the designator. - *

- * E.g.

@ENV:FOO
is given, but
./@ENV:FOO
exists on the filesystem. - */ - public static class AmbiguousInput extends SOPGPException { - - public static final int EXIT_CODE = 73; - - public AmbiguousInput(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Key not signature-capable (e.g. expired, revoked, unacceptable usage flags). - */ - public static class KeyCannotSign extends SOPGPException { - - public static final int EXIT_CODE = 79; - - public KeyCannotSign() { - super(); - } - - public KeyCannotSign(String message) { - super(message); - } - - public KeyCannotSign(String s, Throwable throwable) { - super(s, throwable); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * User provided incompatible options (e.g. "--as=clearsigned --no-armor"). - */ - public static class IncompatibleOptions extends SOPGPException { - - public static final int EXIT_CODE = 83; - - public IncompatibleOptions() { - super(); - } - - public IncompatibleOptions(String errorMsg) { - super(errorMsg); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * The user provided a subcommand with an unsupported profile ("--profile=XYZ"), - * or the user tried to list profiles of a subcommand that does not support profiles at all. - */ - public static class UnsupportedProfile extends SOPGPException { - - public static final int EXIT_CODE = 89; - - private final String subcommand; - private final String profile; - - /** - * Create an exception signalling a subcommand that does not support any profiles. - * - * @param subcommand subcommand - */ - public UnsupportedProfile(String subcommand) { - super("Subcommand '" + subcommand + "' does not support any profiles."); - this.subcommand = subcommand; - this.profile = null; - } - - /** - * Create an exception signalling a subcommand does not support a specific profile. - * - * @param subcommand subcommand - * @param profile unsupported profile - */ - public UnsupportedProfile(String subcommand, String profile) { - super("Subcommand '" + subcommand + "' does not support profile '" + profile + "'."); - this.subcommand = subcommand; - this.profile = profile; - } - - /** - * Wrap an exception into another instance with a possibly translated error message. - * - * @param errorMsg error message - * @param e exception - */ - public UnsupportedProfile(String errorMsg, UnsupportedProfile e) { - super(errorMsg, e); - this.subcommand = e.getSubcommand(); - this.profile = e.getProfile(); - } - - /** - * Return the subcommand name. - * - * @return subcommand - */ - public String getSubcommand() { - return subcommand; - } - - /** - * Return the profile name. - * May return
null
. - * - * @return profile name - */ - public String getProfile() { - return profile; - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } -} diff --git a/sop-java/src/main/java/sop/exception/package-info.java b/sop-java/src/main/java/sop/exception/package-info.java deleted file mode 100644 index 4abc562..0000000 --- a/sop-java/src/main/java/sop/exception/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - * Exception classes. - */ -package sop.exception; diff --git a/sop-java/src/main/java/sop/package-info.java b/sop-java/src/main/java/sop/package-info.java deleted file mode 100644 index 5ad4f52..0000000 --- a/sop-java/src/main/java/sop/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - */ -package sop; diff --git a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt new file mode 100644 index 0000000..2473258 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt @@ -0,0 +1,308 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.exception + +abstract class SOPGPException : RuntimeException { + + constructor() : super() + + constructor(message: String) : super(message) + + constructor(cause: Throwable) : super(cause) + + constructor(message: String, cause: Throwable) : super(message, cause) + + abstract fun getExitCode(): Int + + /** No acceptable signatures found (sop verify, inline-verify). */ + class NoSignature : SOPGPException { + @JvmOverloads + constructor(message: String = "No verifiable signature found.") : super(message) + + constructor(errorMsg: String, e: NoSignature) : super(errorMsg, e) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 3 + } + } + + /** Asymmetric algorithm unsupported (sop encrypt, sign, inline-sign). */ + class UnsupportedAsymmetricAlgo(message: String, e: Throwable) : SOPGPException(message, e) { + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 13 + } + } + + /** Certificate not encryption capable (e,g, expired, revoked, unacceptable usage). */ + class CertCannotEncrypt : SOPGPException { + constructor(message: String, cause: Throwable) : super(message, cause) + + constructor(message: String) : super(message) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 17 + } + } + + /** Missing required argument. */ + class MissingArg : SOPGPException { + constructor() + + constructor(message: String) : super(message) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 19 + } + } + + /** Incomplete verification instructions (sop decrypt). */ + class IncompleteVerification(message: String) : SOPGPException(message) { + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 23 + } + } + + /** Unable to decrypt (sop decrypt). */ + class CannotDecrypt : SOPGPException { + constructor() + + constructor(errorMsg: String, e: Throwable) : super(errorMsg, e) + + constructor(message: String) : super(message) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 29 + } + } + + /** Non-UTF-8 or otherwise unreliable password (sop encrypt). */ + class PasswordNotHumanReadable : SOPGPException { + constructor() : super() + + constructor(message: String) : super(message) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 31 + } + } + + /** Unsupported option. */ + class UnsupportedOption : SOPGPException { + constructor(message: String) : super(message) + + constructor(message: String, cause: Throwable) : super(message, cause) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 37 + } + } + + /** Invalid data type (no secret key where KEYS expected, etc.). */ + class BadData : SOPGPException { + constructor(message: String) : super(message) + + constructor(throwable: Throwable) : super(throwable) + + constructor(message: String, throwable: Throwable) : super(message, throwable) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 41 + } + } + + /** Non-Text input where text expected. */ + class ExpectedText : SOPGPException { + constructor() : super() + + constructor(message: String) : super(message) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 53 + } + } + + /** Output file already exists. */ + class OutputExists(message: String) : SOPGPException(message) { + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 59 + } + } + + /** Input file does not exist. */ + class MissingInput : SOPGPException { + constructor(message: String, cause: Throwable) : super(message, cause) + + constructor(errorMsg: String) : super(errorMsg) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 61 + } + } + + /** A KEYS input is protected (locked) with a password and sop failed to unlock it. */ + class KeyIsProtected : SOPGPException { + constructor() : super() + + constructor(message: String) : super(message) + + constructor(message: String, cause: Throwable) : super(message, cause) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 67 + } + } + + /** Unsupported subcommand. */ + class UnsupportedSubcommand(message: String) : SOPGPException(message) { + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 69 + } + } + + /** + * An indirect parameter is a special designator (it starts with @), but sop does not know how + * to handle the prefix. + */ + class UnsupportedSpecialPrefix(errorMsg: String) : SOPGPException(errorMsg) { + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 71 + } + } + + /** + * Exception that gets thrown if a special designator (starting with @) is given, but the + * filesystem contains a file matching the designator. + * + * E.g.
@ENV:FOO
is given, but
./@ENV:FOO
exists on the filesystem. + */ + class AmbiguousInput(message: String) : SOPGPException(message) { + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 73 + } + } + + /** Key not signature-capable (e.g. expired, revoked, unacceptable usage flags). */ + class KeyCannotSign : SOPGPException { + constructor() : super() + + constructor(message: String) : super(message) + + constructor(s: String, throwable: Throwable) : super(s, throwable) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 79 + } + } + + /** User provided incompatible options (e.g. "--as=clearsigned --no-armor"). */ + class IncompatibleOptions : SOPGPException { + constructor() : super() + + constructor(errorMsg: String) : super(errorMsg) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 83 + } + } + + /** + * The user provided a subcommand with an unsupported profile ("--profile=XYZ"), or the user + * tried to list profiles of a subcommand that does not support profiles at all. + */ + class UnsupportedProfile : SOPGPException { + /** + * Return the subcommand name. + * + * @return subcommand + */ + val subcommand: String + + /** + * Return the profile name. May return `null`. + * + * @return profile name + */ + val profile: String? + + /** + * Create an exception signalling a subcommand that does not support any profiles. + * + * @param subcommand subcommand + */ + constructor( + subcommand: String + ) : super("Subcommand '$subcommand' does not support any profiles.") { + this.subcommand = subcommand + profile = null + } + + /** + * Create an exception signalling a subcommand does not support a specific profile. + * + * @param subcommand subcommand + * @param profile unsupported profile + */ + constructor( + subcommand: String, + profile: String + ) : super("Subcommand '$subcommand' does not support profile '$profile'.") { + this.subcommand = subcommand + this.profile = profile + } + + /** + * Wrap an exception into another instance with a possibly translated error message. + * + * @param errorMsg error message + * @param e exception + */ + constructor(errorMsg: String, e: UnsupportedProfile) : super(errorMsg, e) { + subcommand = e.subcommand + profile = e.profile + } + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 89 + } + } +} From 5ee9414410c76b02b20833c10a84f500dca582aa Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 15:54:54 +0100 Subject: [PATCH 108/298] Encrypt: Add --session-key-out support --- .../main/java/sop/external/ExternalSOP.java | 2 +- .../external/operation/EncryptExternal.java | 62 +++++++++++++++++-- .../sop/cli/picocli/commands/EncryptCmd.java | 36 ++++++++++- .../cli/picocli/commands/EncryptCmdTest.java | 44 ++++++------- .../src/main/kotlin/sop/EncryptionResult.kt | 15 +++++ .../src/main/kotlin/sop/operation/Encrypt.kt | 13 ++-- .../operation/EncryptDecryptTest.java | 30 +++++++-- 7 files changed, 162 insertions(+), 40 deletions(-) create mode 100644 sop-java/src/main/kotlin/sop/EncryptionResult.kt diff --git a/external-sop/src/main/java/sop/external/ExternalSOP.java b/external-sop/src/main/java/sop/external/ExternalSOP.java index 376e54c..e1479ee 100644 --- a/external-sop/src/main/java/sop/external/ExternalSOP.java +++ b/external-sop/src/main/java/sop/external/ExternalSOP.java @@ -147,7 +147,7 @@ public class ExternalSOP implements SOP { @Override public Encrypt encrypt() { - return new EncryptExternal(binaryName, properties); + return new EncryptExternal(binaryName, properties, tempDirProvider); } @Override diff --git a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java index bc40208..3eabfc5 100644 --- a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java +++ b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java @@ -4,14 +4,19 @@ package sop.external.operation; -import sop.Ready; +import sop.EncryptionResult; +import sop.ReadyWithResult; +import sop.SessionKey; import sop.enums.EncryptAs; import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.Encrypt; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import java.util.Properties; @@ -21,6 +26,7 @@ import java.util.Properties; */ public class EncryptExternal implements Encrypt { + private final ExternalSOP.TempDirProvider tempDirProvider; private final List commandList = new ArrayList<>(); private final List envList; private int SIGN_WITH_COUNTER = 0; @@ -28,7 +34,8 @@ public class EncryptExternal implements Encrypt { private int PASSWORD_COUNTER = 0; private int CERT_COUNTER = 0; - public EncryptExternal(String binary, Properties environment) { + public EncryptExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) { + this.tempDirProvider = tempDirProvider; this.commandList.add(binary); this.commandList.add("encrypt"); this.envList = ExternalSOP.propertiesToEnv(environment); @@ -92,8 +99,53 @@ public class EncryptExternal implements Encrypt { } @Override - public Ready plaintext(InputStream plaintext) - throws SOPGPException.KeyIsProtected { - return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, plaintext); + public ReadyWithResult plaintext(InputStream plaintext) + throws SOPGPException.KeyIsProtected, IOException { + File tempDir = tempDirProvider.provideTempDirectory(); + + File sessionKeyOut = new File(tempDir, "session-key-out"); + sessionKeyOut.delete(); + commandList.add("--session-key-out=" + sessionKeyOut.getAbsolutePath()); + + String[] command = commandList.toArray(new String[0]); + String[] env = envList.toArray(new String[0]); + try { + Process process = Runtime.getRuntime().exec(command, env); + OutputStream processOut = process.getOutputStream(); + InputStream processIn = process.getInputStream(); + + return new ReadyWithResult() { + @Override + public EncryptionResult writeTo(OutputStream outputStream) throws IOException { + byte[] buf = new byte[4096]; + int r; + while ((r = plaintext.read(buf)) > 0) { + processOut.write(buf, 0, r); + } + + plaintext.close(); + processOut.close(); + + while ((r = processIn.read(buf)) > 0) { + outputStream.write(buf, 0 , r); + } + + processIn.close(); + outputStream.close(); + + ExternalSOP.finish(process); + + FileInputStream sessionKeyOutIn = new FileInputStream(sessionKeyOut); + String line = ExternalSOP.readString(sessionKeyOutIn); + SessionKey sessionKey = SessionKey.fromString(line.trim()); + sessionKeyOutIn.close(); + sessionKeyOut.delete(); + + return new EncryptionResult(sessionKey); + } + }; + } catch (IOException e) { + throw new RuntimeException(e); + } } } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java index efda26f..0d37416 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java @@ -5,7 +5,9 @@ package sop.cli.picocli.commands; import picocli.CommandLine; -import sop.Ready; +import sop.EncryptionResult; +import sop.ReadyWithResult; +import sop.SessionKey; import sop.cli.picocli.SopCLI; import sop.enums.EncryptAs; import sop.exception.SOPGPException; @@ -13,6 +15,8 @@ import sop.operation.Encrypt; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -49,11 +53,18 @@ public class EncryptCmd extends AbstractSopCmd { paramLabel = "CERTS") List certs = new ArrayList<>(); + @CommandLine.Option( + names = {"--session-key-out"}, + paramLabel = "SESSIONKEY") + String sessionKeyOut; + @Override public void run() { Encrypt encrypt = throwIfUnsupportedSubcommand( SopCLI.getSop().encrypt(), "encrypt"); + throwIfOutputExists(sessionKeyOut); + if (profile != null) { try { encrypt.profile(profile); @@ -145,8 +156,27 @@ public class EncryptCmd extends AbstractSopCmd { } try { - Ready ready = encrypt.plaintext(System.in); - ready.writeTo(System.out); + ReadyWithResult ready = encrypt.plaintext(System.in); + EncryptionResult result = ready.writeTo(System.out); + + if (sessionKeyOut == null) { + return; + } + try (OutputStream outputStream = getOutput(sessionKeyOut)) { + if (!result.getSessionKey().isPresent()) { + String errorMsg = getMsg("sop.error.runtime.no_session_key_extracted"); + throw new SOPGPException.UnsupportedOption(String.format(errorMsg, "--session-key-out")); + } + SessionKey sessionKey = result.getSessionKey().get(); + if (sessionKey == null) { + return; + } + PrintWriter writer = new PrintWriter(outputStream); + // CHECKSTYLE:OFF + writer.println(sessionKey); + // CHECKSTYLE:ON + writer.flush(); + } } catch (IOException e) { throw new RuntimeException(e); } 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 73ec9cb..09346af 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,6 +4,24 @@ 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; +import sop.EncryptionResult; +import sop.ReadyWithResult; +import sop.SOP; +import sop.cli.picocli.SopCLI; +import sop.cli.picocli.TestFileUtil; +import sop.enums.EncryptAs; +import sop.exception.SOPGPException; +import sop.operation.Encrypt; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -11,22 +29,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -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.Ready; -import sop.SOP; -import sop.cli.picocli.SopCLI; -import sop.cli.picocli.TestFileUtil; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.operation.Encrypt; - public class EncryptCmdTest { Encrypt encrypt; @@ -34,10 +36,10 @@ public class EncryptCmdTest { @BeforeEach public void mockComponents() throws IOException { encrypt = mock(Encrypt.class); - when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() { + when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult() { @Override - public void writeTo(OutputStream outputStream) { - + public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException { + return new EncryptionResult(null); } }); @@ -190,9 +192,9 @@ public class EncryptCmdTest { @Test @ExpectSystemExitWithStatus(1) public void writeTo_ioExceptionCausesExit1() throws IOException { - when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() { + when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException { throw new IOException(); } }); diff --git a/sop-java/src/main/kotlin/sop/EncryptionResult.kt b/sop-java/src/main/kotlin/sop/EncryptionResult.kt new file mode 100644 index 0000000..1284f89 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/EncryptionResult.kt @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import sop.util.Optional + +class EncryptionResult(sessionKey: SessionKey?) { + val sessionKey: Optional + + init { + this.sessionKey = Optional.ofNullable(sessionKey) + } +} diff --git a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt index 0daebee..71c04cb 100644 --- a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt +++ b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt @@ -6,8 +6,9 @@ package sop.operation import java.io.IOException import java.io.InputStream +import sop.EncryptionResult import sop.Profile -import sop.Ready +import sop.ReadyWithResult import sop.enums.EncryptAs import sop.exception.SOPGPException.* import sop.util.UTF8Util @@ -146,20 +147,22 @@ interface Encrypt { * Encrypt the given data yielding the ciphertext. * * @param plaintext plaintext - * @return input stream containing the ciphertext + * @return result and ciphertext * @throws IOException in case of an IO error * @throws KeyIsProtected if at least one signing key cannot be unlocked */ - @Throws(IOException::class, KeyIsProtected::class) fun plaintext(plaintext: InputStream): Ready + @Throws(IOException::class, KeyIsProtected::class) + fun plaintext(plaintext: InputStream): ReadyWithResult /** * Encrypt the given data yielding the ciphertext. * * @param plaintext plaintext - * @return input stream containing the ciphertext + * @return result and ciphertext * @throws IOException in case of an IO error * @throws KeyIsProtected if at least one signing key cannot be unlocked */ @Throws(IOException::class, KeyIsProtected::class) - fun plaintext(plaintext: ByteArray): Ready = plaintext(plaintext.inputStream()) + fun plaintext(plaintext: ByteArray): ReadyWithResult = + plaintext(plaintext.inputStream()) } 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 9138f64..51c117a 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java @@ -10,13 +10,16 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import sop.ByteArrayAndResult; import sop.DecryptionResult; +import sop.EncryptionResult; import sop.SOP; +import sop.SessionKey; import sop.Verification; import sop.enums.EncryptAs; import sop.enums.SignatureMode; import sop.exception.SOPGPException; import sop.testsuite.TestData; import sop.testsuite.assertions.VerificationListAssert; +import sop.util.Optional; import sop.util.UTCUtil; import java.io.IOException; @@ -27,6 +30,7 @@ import java.util.List; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -41,18 +45,26 @@ public class EncryptDecryptTest extends AbstractSOPTest { @MethodSource("provideInstances") public void encryptDecryptRoundTripPasswordTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + ByteArrayAndResult encResult = sop.encrypt() .withPassword("sw0rdf1sh") .plaintext(message) - .getBytes(); + .toByteArrayAndResult(); - byte[] plaintext = sop.decrypt() + byte[] ciphertext = encResult.getBytes(); + Optional encSessionKey = encResult.getResult().getSessionKey(); + + ByteArrayAndResult decResult = sop.decrypt() .withPassword("sw0rdf1sh") .ciphertext(ciphertext) - .toByteArrayAndResult() - .getBytes(); + .toByteArrayAndResult(); + + byte[] plaintext = decResult.getBytes(); + Optional decSessionKey = decResult.getResult().getSessionKey(); assertArrayEquals(message, plaintext); + if (encSessionKey.isPresent() && decSessionKey.isPresent()) { + assertEquals(encSessionKey.get(), decSessionKey.get()); + } } @ParameterizedTest @@ -62,6 +74,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] ciphertext = sop.encrypt() .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -83,6 +96,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] ciphertext = sop.encrypt() .withCert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .plaintext(message) + .toByteArrayAndResult() .getBytes(); byte[] plaintext = sop.decrypt() @@ -101,6 +115,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] ciphertext = sop.encrypt() .withCert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8)) .plaintext(message) + .toByteArrayAndResult() .getBytes(); byte[] plaintext = sop.decrypt() @@ -120,6 +135,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .noArmor() .plaintext(message) + .toByteArrayAndResult() .getBytes(); byte[] armored = sop.armor() @@ -144,6 +160,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .mode(EncryptAs.binary) .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -175,6 +192,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .mode(EncryptAs.text) .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -215,6 +233,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { .signWith(key) .withKeyPassword(keyPassword) .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -305,6 +324,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { assertThrows(SOPGPException.MissingArg.class, () -> sop.encrypt() .plaintext(message) + .toByteArrayAndResult() .getBytes()); } } From 86b173bf1c4b5daa4b58abf2390597b7eee0d73b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 16:16:32 +0100 Subject: [PATCH 109/298] Update Changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ca12a2..d736ded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 8.0.0-SNAPSHOT +- Rewrote API in Kotlin +- Update implementation to [SOP Specification revision 08](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html). + - Add `--no-armor` option to `revoke-key` and `change-key-password` subcommands + - `armor`: Deprecate `--label` option + - `encrypt`: Add `--session-key-out` option +- Slight API changes: + - `sop.encrypt().plaintext()` now returns a `ReadyWithResult` instead of `Ready`. + - `EncryptionResult` is a new result type, that provides access to the session key of an encrypted message + - Change `ArmorLabel` values into lowercase + - Change `EncryptAs` values into lowercase + - Change `SignAs` values into lowercase + ## 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 1de179c0156c7bad489f05bd4c0a39cf06f96286 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 16:07:37 +0100 Subject: [PATCH 110/298] Kotlin conversion: VersionCmd --- .../sop/cli/picocli/commands/VersionCmd.java | 58 ------------------- .../sop/cli/picocli/commands/VersionCmd.kt | 51 ++++++++++++++++ 2 files changed, 51 insertions(+), 58 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java deleted file mode 100644 index 6ccb8f7..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Version; - -@CommandLine.Command(name = "version", resourceBundle = "msg_version", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class VersionCmd extends AbstractSopCmd { - - @CommandLine.ArgGroup() - Exclusive exclusive; - - static class Exclusive { - @CommandLine.Option(names = "--extended") - boolean extended; - - @CommandLine.Option(names = "--backend") - boolean backend; - - @CommandLine.Option(names = "--sop-spec") - boolean sopSpec; - } - - - - @Override - public void run() { - Version version = throwIfUnsupportedSubcommand( - SopCLI.getSop().version(), "version"); - - if (exclusive == null) { - Print.outln(version.getName() + " " + version.getVersion()); - return; - } - - if (exclusive.extended) { - Print.outln(version.getExtendedVersion()); - return; - } - - if (exclusive.backend) { - Print.outln(version.getBackendVersion()); - return; - } - - if (exclusive.sopSpec) { - Print.outln(version.getSopSpecVersion()); - return; - } - } -} 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 new file mode 100644 index 0000000..75197fe --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import picocli.CommandLine.ArgGroup +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException + +@Command( + name = "version", + resourceBundle = "msg_version", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class VersionCmd : AbstractSopCmd() { + + @ArgGroup var exclusive: Exclusive? = null + + class Exclusive { + @Option(names = ["--extended"]) var extended: Boolean = false + @Option(names = ["--backend"]) var backend: Boolean = false + @Option(names = ["--sop-spec"]) var sopSpec: Boolean = false + } + + override fun run() { + val version = throwIfUnsupportedSubcommand(SopCLI.getSop().version(), "version") + + if (exclusive == null) { + // No option provided + println("${version.getName()} ${version.getVersion()}") + return + } + + if (exclusive!!.extended) { + println(version.getExtendedVersion()) + return + } + + if (exclusive!!.backend) { + println(version.getBackendVersion()) + return + } + + if (exclusive!!.sopSpec) { + println(version.getSopSpecVersion()) + return + } + } +} From 8246359a854ec3d2fe715ccf683e908afb981c68 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 16:15:35 +0100 Subject: [PATCH 111/298] Kotlin conversion: VerifyCmd --- .../sop/cli/picocli/commands/VerifyCmd.java | 102 ------------------ .../sop/cli/picocli/commands/VerifyCmd.kt | 81 ++++++++++++++ 2 files changed, 81 insertions(+), 102 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VerifyCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java deleted file mode 100644 index d76bb37..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Verification; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.DetachedVerify; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "verify", - resourceBundle = "msg_detached-verify", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class VerifyCmd extends AbstractSopCmd { - - @CommandLine.Parameters(index = "0", - paramLabel = "SIGNATURE") - String signature; - - @CommandLine.Parameters(index = "1..*", - arity = "1..*", - paramLabel = "CERT") - List certificates = new ArrayList<>(); - - @CommandLine.Option(names = {"--not-before"}, - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {"--not-after"}, - paramLabel = "DATE") - String notAfter = "now"; - - @Override - public void run() { - DetachedVerify detachedVerify = throwIfUnsupportedSubcommand( - SopCLI.getSop().detachedVerify(), "verify"); - - if (notAfter != null) { - try { - detachedVerify.notAfter(parseNotAfter(notAfter)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - if (notBefore != null) { - try { - detachedVerify.notBefore(parseNotBefore(notBefore)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - for (String certInput : certificates) { - try (InputStream certIn = getInput(certInput)) { - detachedVerify.cert(certIn); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - if (signature != null) { - try (InputStream sigIn = getInput(signature)) { - detachedVerify.signatures(sigIn); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_signature", signature); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - List verifications; - try { - verifications = detachedVerify.data(System.in); - } catch (SOPGPException.NoSignature e) { - String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found"); - throw new SOPGPException.NoSignature(errorMsg, e); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.stdin_not_a_message"); - throw new SOPGPException.BadData(errorMsg, badData); - } - - for (Verification verification : verifications) { - Print.outln(verification.toString()); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VerifyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VerifyCmd.kt new file mode 100644 index 0000000..ef27266 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VerifyCmd.kt @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.* + +@Command( + name = "verify", + resourceBundle = "msg_detached-verify", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class VerifyCmd : AbstractSopCmd() { + + @Parameters(index = "0", paramLabel = "SIGNATURE") lateinit var signature: String + + @Parameters(index = "1..*", arity = "1..*", paramLabel = "CERT") + lateinit var certificates: List + + @Option(names = ["--not-before"], paramLabel = "DATE") var notBefore: String = "-" + + @Option(names = ["--not-after"], paramLabel = "DATE") var notAfter: String = "now" + + override fun run() { + val detachedVerify = + throwIfUnsupportedSubcommand(SopCLI.getSop().detachedVerify(), "verify") + try { + detachedVerify.notAfter(parseNotAfter(notAfter)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + try { + detachedVerify.notBefore(parseNotBefore(notBefore)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + for (certInput in certificates) { + try { + getInput(certInput).use { certIn -> detachedVerify.cert(certIn) } + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput) + throw BadData(errorMsg, badData) + } + } + + try { + getInput(signature).use { sigIn -> detachedVerify.signatures(sigIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_signature", signature) + throw BadData(errorMsg, badData) + } + + val verifications = + try { + detachedVerify.data(System.`in`) + } catch (e: NoSignature) { + val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found") + throw NoSignature(errorMsg, e) + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_a_message") + throw BadData(errorMsg, badData) + } + + for (verification in verifications) { + println(verification.toString()) + } + } +} From 256d1c596067fa91ff0d542981a2ca8f9141deae Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 16:34:35 +0100 Subject: [PATCH 112/298] Kotlin conversion: SignCmd --- .../sop/cli/picocli/commands/SignCmd.java | 108 ------------------ .../sop/cli/picocli/commands/SignCmd.kt | 90 +++++++++++++++ 2 files changed, 90 insertions(+), 108 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/SignCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java deleted file mode 100644 index cad9d6e..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.MicAlg; -import sop.ReadyWithResult; -import sop.SigningResult; -import sop.cli.picocli.SopCLI; -import sop.enums.SignAs; -import sop.exception.SOPGPException; -import sop.operation.DetachedSign; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "sign", - resourceBundle = "msg_detached-sign", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class SignCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = "--as", - paramLabel = "{binary|text}") - SignAs type; - - @CommandLine.Parameters(paramLabel = "KEYS") - List secretKeyFile = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - paramLabel = "PASSWORD") - List withKeyPassword = new ArrayList<>(); - - @CommandLine.Option(names = "--micalg-out", - paramLabel = "MICALG") - String micAlgOut; - - @Override - public void run() { - DetachedSign detachedSign = throwIfUnsupportedSubcommand( - SopCLI.getSop().detachedSign(), "sign"); - - throwIfOutputExists(micAlgOut); - throwIfEmptyParameters(secretKeyFile, "KEYS"); - - if (type != null) { - try { - detachedSign.mode(type); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - for (String passwordFile : withKeyPassword) { - try { - String password = stringFromInputStream(getInput(passwordFile)); - detachedSign.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - for (String keyInput : secretKeyFile) { - try (InputStream keyIn = getInput(keyInput)) { - detachedSign.key(keyIn); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.KeyIsProtected keyIsProtected) { - String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput); - throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - if (!armor) { - detachedSign.noArmor(); - } - - try { - ReadyWithResult ready = detachedSign.data(System.in); - SigningResult result = ready.writeTo(System.out); - - MicAlg micAlg = result.getMicAlg(); - if (micAlgOut != null) { - // Write micalg out - OutputStream outputStream = getOutput(micAlgOut); - micAlg.writeTo(outputStream); - outputStream.close(); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/SignCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/SignCmd.kt new file mode 100644 index 0000000..6860477 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/SignCmd.kt @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.enums.SignAs +import sop.exception.SOPGPException +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.KeyIsProtected + +@Command( + name = "sign", + resourceBundle = "msg_detached-sign", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class SignCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true + + @Option(names = ["--as"], paramLabel = "{binary|text}") var type: SignAs? = null + + @Parameters(paramLabel = "KEYS") var secretKeyFile: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + @Option(names = ["--micalg-out"], paramLabel = "MICALG") var micAlgOut: String? = null + + override fun run() { + val detachedSign = throwIfUnsupportedSubcommand(SopCLI.getSop().detachedSign(), "sign") + + throwIfOutputExists(micAlgOut) + throwIfEmptyParameters(secretKeyFile, "KEYS") + + try { + type?.let { detachedSign.mode(it) } + } catch (unsupported: SOPGPException.UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw SOPGPException.UnsupportedOption(errorMsg, unsupported) + } catch (ioe: IOException) { + throw RuntimeException(ioe) + } + + withKeyPassword.forEach { passIn -> + try { + val password = stringFromInputStream(getInput(passIn)) + detachedSign.withKeyPassword(password) + } catch (unsupported: SOPGPException.UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw SOPGPException.UnsupportedOption(errorMsg, unsupported) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + secretKeyFile.forEach { keyIn -> + try { + getInput(keyIn).use { input -> detachedSign.key(input) } + } catch (ioe: IOException) { + throw RuntimeException(ioe) + } catch (keyIsProtected: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyIn) + throw KeyIsProtected(errorMsg, keyIsProtected) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", keyIn) + throw BadData(errorMsg, badData) + } + } + + if (!armor) { + detachedSign.noArmor() + } + + try { + val ready = detachedSign.data(System.`in`) + val result = ready.writeTo(System.out) + + if (micAlgOut != null) { + getOutput(micAlgOut).use { result.micAlg.writeTo(it) } + } + } catch (e: IOException) { + throw java.lang.RuntimeException(e) + } + } +} From 666d51384b442ffa8621299b7874a1fabba6c020 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 17:20:00 +0100 Subject: [PATCH 113/298] Kotlin conversion: AbstractSopCmd --- .../cli/picocli/commands/AbstractSopCmd.java | 282 ------------------ .../cli/picocli/commands/AbstractSopCmd.kt | 248 +++++++++++++++ .../picocli/commands/AbstractSopCmdTest.java | 2 +- 3 files changed, 249 insertions(+), 283 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java deleted file mode 100644 index 9aec5a7..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java +++ /dev/null @@ -1,282 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import sop.exception.SOPGPException; -import sop.util.UTCUtil; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.text.ParseException; -import java.util.Collection; -import java.util.Date; -import java.util.Locale; -import java.util.ResourceBundle; -import java.util.regex.Pattern; - -/** - * Abstract super class of SOP subcommands. - */ -public abstract class AbstractSopCmd implements Runnable { - - /** - * Interface to modularize resolving of environment variables. - */ - public interface EnvironmentVariableResolver { - /** - * Resolve the value of the given environment variable. - * Return null if the variable is not present. - * - * @param name name of the variable - * @return variable value or null - */ - String resolveEnvironmentVariable(String name); - } - - public static final String PRFX_ENV = "@ENV:"; - public static final String PRFX_FD = "@FD:"; - public static final Date BEGINNING_OF_TIME = new Date(0); - public static final Date END_OF_TIME = new Date(8640000000000000L); - - public static final Pattern PATTERN_FD = Pattern.compile("^\\d{1,20}$"); - - protected final ResourceBundle messages; - protected EnvironmentVariableResolver envResolver = System::getenv; - - public AbstractSopCmd() { - this(Locale.getDefault()); - } - - public AbstractSopCmd(@Nonnull Locale locale) { - messages = ResourceBundle.getBundle("msg_sop", locale); - } - - void throwIfOutputExists(String output) { - if (output == null) { - return; - } - - File outputFile = new File(output); - if (outputFile.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.output_file_already_exists", outputFile.getAbsolutePath()); - throw new SOPGPException.OutputExists(errorMsg); - } - } - - public String getMsg(String key) { - return messages.getString(key); - } - - public String getMsg(String key, String arg1) { - return String.format(messages.getString(key), arg1); - } - - public String getMsg(String key, String arg1, String arg2) { - return String.format(messages.getString(key), arg1, arg2); - } - - void throwIfMissingArg(Object arg, String argName) { - if (arg == null) { - String errorMsg = getMsg("sop.error.usage.argument_required", argName); - throw new SOPGPException.MissingArg(errorMsg); - } - } - - void throwIfEmptyParameters(Collection arg, String parmName) { - if (arg.isEmpty()) { - String errorMsg = getMsg("sop.error.usage.parameter_required", parmName); - throw new SOPGPException.MissingArg(errorMsg); - } - } - - T throwIfUnsupportedSubcommand(T subcommand, String subcommandName) { - if (subcommand == null) { - String errorMsg = getMsg("sop.error.feature_support.subcommand_not_supported", subcommandName); - throw new SOPGPException.UnsupportedSubcommand(errorMsg); - } - return subcommand; - } - - void setEnvironmentVariableResolver(EnvironmentVariableResolver envResolver) { - if (envResolver == null) { - throw new NullPointerException("Variable envResolver cannot be null."); - } - this.envResolver = envResolver; - } - - public InputStream getInput(String indirectInput) throws IOException { - if (indirectInput == null) { - throw new IllegalArgumentException("Input cannot not be null."); - } - - String trimmed = indirectInput.trim(); - if (trimmed.isEmpty()) { - throw new IllegalArgumentException("Input cannot be blank."); - } - - if (trimmed.startsWith(PRFX_ENV)) { - if (new File(trimmed).exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed); - throw new SOPGPException.AmbiguousInput(errorMsg); - } - - String envName = trimmed.substring(PRFX_ENV.length()); - String envValue = envResolver.resolveEnvironmentVariable(envName); - if (envValue == null) { - String errorMsg = getMsg("sop.error.indirect_data_type.environment_variable_not_set", envName); - throw new IllegalArgumentException(errorMsg); - } - - if (envValue.trim().isEmpty()) { - String errorMsg = getMsg("sop.error.indirect_data_type.environment_variable_empty", envName); - throw new IllegalArgumentException(errorMsg); - } - - return new ByteArrayInputStream(envValue.getBytes("UTF8")); - - } else if (trimmed.startsWith(PRFX_FD)) { - - if (new File(trimmed).exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed); - throw new SOPGPException.AmbiguousInput(errorMsg); - } - - File fdFile = fileDescriptorFromString(trimmed); - try { - FileInputStream fileIn = new FileInputStream(fdFile); - return fileIn; - } catch (FileNotFoundException e) { - String errorMsg = getMsg("sop.error.indirect_data_type.file_descriptor_not_found", fdFile.getAbsolutePath()); - throw new IOException(errorMsg, e); - } - } else { - File file = new File(trimmed); - if (!file.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.input_file_does_not_exist", file.getAbsolutePath()); - throw new SOPGPException.MissingInput(errorMsg); - } - - if (!file.isFile()) { - String errorMsg = getMsg("sop.error.indirect_data_type.input_not_a_file", file.getAbsolutePath()); - throw new SOPGPException.MissingInput(errorMsg); - } - - return new FileInputStream(file); - } - } - - public OutputStream getOutput(String indirectOutput) throws IOException { - if (indirectOutput == null) { - throw new IllegalArgumentException("Output cannot be null."); - } - - String trimmed = indirectOutput.trim(); - if (trimmed.isEmpty()) { - throw new IllegalArgumentException("Output cannot be blank."); - } - - // @ENV not allowed for output - if (trimmed.startsWith(PRFX_ENV)) { - String errorMsg = getMsg("sop.error.indirect_data_type.illegal_use_of_env_designator"); - throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg); - } - - // File Descriptor - if (trimmed.startsWith(PRFX_FD)) { - File fdFile = fileDescriptorFromString(trimmed); - try { - FileOutputStream fout = new FileOutputStream(fdFile); - return fout; - } catch (FileNotFoundException e) { - String errorMsg = getMsg("sop.error.indirect_data_type.file_descriptor_not_found", fdFile.getAbsolutePath()); - throw new IOException(errorMsg, e); - } - } - - File file = new File(trimmed); - if (file.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.output_file_already_exists", file.getAbsolutePath()); - throw new SOPGPException.OutputExists(errorMsg); - } - - if (!file.createNewFile()) { - String errorMsg = getMsg("sop.error.indirect_data_type.output_file_cannot_be_created", file.getAbsolutePath()); - throw new IOException(errorMsg); - } - - return new FileOutputStream(file); - } - - public File fileDescriptorFromString(String fdString) { - File fdDir = new File("/dev/fd/"); - if (!fdDir.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.designator_fd_not_supported"); - throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg); - } - String fdNumber = fdString.substring(PRFX_FD.length()); - if (!PATTERN_FD.matcher(fdNumber).matches()) { - throw new IllegalArgumentException("File descriptor must be a positive number."); - } - File descriptor = new File(fdDir, fdNumber); - return descriptor; - } - - public static String stringFromInputStream(InputStream inputStream) throws IOException { - try { - ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); - byte[] buf = new byte[4096]; int read; - while ((read = inputStream.read(buf)) != -1) { - byteOut.write(buf, 0, read); - } - // TODO: For decrypt operations we MUST accept non-UTF8 passwords - return UTF8Util.decodeUTF8(byteOut.toByteArray()); - } finally { - inputStream.close(); - } - } - - public Date parseNotAfter(String notAfter) { - if (notAfter.equals("now")) { - return new Date(); - } - - if (notAfter.equals("-")) { - return END_OF_TIME; - } - - try { - return UTCUtil.parseUTCDate(notAfter); - } catch (ParseException e) { - String errorMsg = getMsg("sop.error.input.malformed_not_after"); - throw new IllegalArgumentException(errorMsg); - } - } - - public Date parseNotBefore(String notBefore) { - if (notBefore.equals("now")) { - return new Date(); - } - - if (notBefore.equals("-")) { - return BEGINNING_OF_TIME; - } - - try { - return UTCUtil.parseUTCDate(notBefore); - } catch (ParseException e) { - String errorMsg = getMsg("sop.error.input.malformed_not_before"); - throw new IllegalArgumentException(errorMsg); - } - } -} 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 new file mode 100644 index 0000000..4629e57 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.* +import java.text.ParseException +import java.util.* +import sop.cli.picocli.commands.AbstractSopCmd.EnvironmentVariableResolver +import sop.exception.SOPGPException.* +import sop.util.UTCUtil.Companion.parseUTCDate +import sop.util.UTF8Util.Companion.decodeUTF8 + +/** Abstract super class of SOP subcommands. */ +abstract class AbstractSopCmd(locale: Locale = Locale.getDefault()) : Runnable { + + private val messages: ResourceBundle = ResourceBundle.getBundle("msg_sop", locale) + var environmentVariableResolver = EnvironmentVariableResolver { name: String -> + System.getenv(name) + } + + /** Interface to modularize resolving of environment variables. */ + fun interface EnvironmentVariableResolver { + + /** + * Resolve the value of the given environment variable. Return null if the variable is not + * present. + * + * @param name name of the variable + * @return variable value or null + */ + fun resolveEnvironmentVariable(name: String): String? + } + + fun throwIfOutputExists(output: String?) { + output + ?.let { File(it) } + ?.let { + if (it.exists()) { + val errorMsg: String = + getMsg( + "sop.error.indirect_data_type.output_file_already_exists", + it.absolutePath) + throw OutputExists(errorMsg) + } + } + } + + fun getMsg(key: String): String = messages.getString(key) + + fun getMsg(key: String, vararg args: String): String { + val msg = messages.getString(key) + return String.format(msg, *args) + } + + fun throwIfMissingArg(arg: Any?, argName: String) { + if (arg == null) { + val errorMsg = getMsg("sop.error.usage.argument_required", argName) + throw MissingArg(errorMsg) + } + } + + fun throwIfEmptyParameters(arg: Collection<*>, parmName: String) { + if (arg.isEmpty()) { + val errorMsg = getMsg("sop.error.usage.parameter_required", parmName) + throw MissingArg(errorMsg) + } + } + + fun throwIfUnsupportedSubcommand(subcommand: T?, subcommandName: String): T { + if (subcommand == null) { + val errorMsg = + getMsg("sop.error.feature_support.subcommand_not_supported", subcommandName) + throw UnsupportedSubcommand(errorMsg) + } + return subcommand + } + + @Throws(IOException::class) + fun getInput(indirectInput: String): InputStream { + val trimmed = indirectInput.trim() + require(trimmed.isNotBlank()) { "Input cannot be blank." } + + if (trimmed.startsWith(PRFX_ENV)) { + if (File(trimmed).exists()) { + val errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed) + throw AmbiguousInput(errorMsg) + } + + val envName = trimmed.substring(PRFX_ENV.length) + val envValue = environmentVariableResolver.resolveEnvironmentVariable(envName) + requireNotNull(envValue) { + getMsg("sop.error.indirect_data_type.environment_variable_not_set", envName) + } + + require(envValue.trim().isNotEmpty()) { + getMsg("sop.error.indirect_data_type.environment_variable_empty", envName) + } + + return envValue.byteInputStream() + } else if (trimmed.startsWith(PRFX_FD)) { + + if (File(trimmed).exists()) { + val errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed) + throw AmbiguousInput(errorMsg) + } + + val fdFile: File = fileDescriptorFromString(trimmed) + return try { + fdFile.inputStream() + } catch (e: FileNotFoundException) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.file_descriptor_not_found", + fdFile.absolutePath) + throw IOException(errorMsg, e) + } + } else { + + val file = File(trimmed) + if (!file.exists()) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.input_file_does_not_exist", file.absolutePath) + throw MissingInput(errorMsg) + } + if (!file.isFile()) { + val errorMsg = + getMsg("sop.error.indirect_data_type.input_not_a_file", file.absolutePath) + throw MissingInput(errorMsg) + } + return file.inputStream() + } + } + + @Throws(IOException::class) + fun getOutput(indirectOutput: String?): OutputStream { + requireNotNull(indirectOutput) { "Output cannot be null." } + val trimmed = indirectOutput.trim() + require(trimmed.isNotEmpty()) { "Output cannot be blank." } + + // @ENV not allowed for output + if (trimmed.startsWith(PRFX_ENV)) { + val errorMsg = getMsg("sop.error.indirect_data_type.illegal_use_of_env_designator") + throw UnsupportedSpecialPrefix(errorMsg) + } + + // File Descriptor + if (trimmed.startsWith(PRFX_FD)) { + val fdFile = fileDescriptorFromString(trimmed) + return try { + fdFile.outputStream() + } catch (e: FileNotFoundException) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.file_descriptor_not_found", + fdFile.absolutePath) + throw IOException(errorMsg, e) + } + } + val file = File(trimmed) + if (file.exists()) { + val errorMsg = + getMsg("sop.error.indirect_data_type.output_file_already_exists", file.absolutePath) + throw OutputExists(errorMsg) + } + if (!file.createNewFile()) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.output_file_cannot_be_created", file.absolutePath) + throw IOException(errorMsg) + } + return file.outputStream() + } + + fun fileDescriptorFromString(fdString: String): File { + val fdDir = File("/dev/fd/") + if (!fdDir.exists()) { + val errorMsg = getMsg("sop.error.indirect_data_type.designator_fd_not_supported") + throw UnsupportedSpecialPrefix(errorMsg) + } + val fdNumber = fdString.substring(PRFX_FD.length) + require(PATTERN_FD.matcher(fdNumber).matches()) { + "File descriptor must be a positive number." + } + return File(fdDir, fdNumber) + } + + fun parseNotAfter(notAfter: String): Date { + return when (notAfter) { + "now" -> Date() + "-" -> END_OF_TIME + else -> + try { + parseUTCDate(notAfter) + } catch (e: ParseException) { + val errorMsg = getMsg("sop.error.input.malformed_not_after") + throw IllegalArgumentException(errorMsg) + } + } + } + + fun parseNotBefore(notBefore: String): Date { + return when (notBefore) { + "now" -> Date() + "-" -> DAWN_OF_TIME + else -> + try { + parseUTCDate(notBefore) + } catch (e: ParseException) { + val errorMsg = getMsg("sop.error.input.malformed_not_before") + throw IllegalArgumentException(errorMsg) + } + } + } + + companion object { + const val PRFX_ENV = "@ENV:" + + const val PRFX_FD = "@FD:" + + @JvmField val DAWN_OF_TIME = Date(0) + + @JvmField + @Deprecated("Replace with DAWN_OF_TIME", ReplaceWith("DAWN_OF_TIME")) + val BEGINNING_OF_TIME = DAWN_OF_TIME + + @JvmField val END_OF_TIME = Date(8640000000000000L) + + @JvmField val PATTERN_FD = "^\\d{1,20}$".toPattern() + + @Throws(IOException::class) + @JvmStatic + fun stringFromInputStream(inputStream: InputStream): String { + return inputStream.use { input -> + val byteOut = ByteArrayOutputStream() + val buf = ByteArray(4096) + var read: Int + while (input.read(buf).also { read = it } != -1) { + byteOut.write(buf, 0, read) + } + // TODO: For decrypt operations we MUST accept non-UTF8 passwords + decodeUTF8(byteOut.toByteArray()) + } + } + } +} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java index aed420b..396bc7f 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java @@ -36,7 +36,7 @@ public class AbstractSopCmdTest { @Test public void getInput_NullInvalid() { - assertThrows(IllegalArgumentException.class, () -> abstractCmd.getInput(null)); + assertThrows(NullPointerException.class, () -> abstractCmd.getInput(null)); } @Test From 18865feaff9192ac63d6f6a44b1ec6f3648621ce Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 17:26:38 +0100 Subject: [PATCH 114/298] Kotlin conversion: RevokeKeyCmd --- .../cli/picocli/commands/RevokeKeyCmd.java | 62 ------------------- .../sop/cli/picocli/commands/RevokeKeyCmd.kt | 58 +++++++++++++++++ 2 files changed, 58 insertions(+), 62 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java deleted file mode 100644 index 45f22fa..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.RevokeKey; - -import java.io.IOException; - -@CommandLine.Command(name = "revoke-key", - resourceBundle = "msg_revoke-key", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class RevokeKeyCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = "--with-key-password", - paramLabel = "PASSWORD") - String withKeyPassword; - - @Override - public void run() { - RevokeKey revokeKey = throwIfUnsupportedSubcommand( - SopCLI.getSop().revokeKey(), "revoke-key"); - - if (!armor) { - revokeKey.noArmor(); - } - - if (withKeyPassword != null) { - try { - String password = stringFromInputStream(getInput(withKeyPassword)); - revokeKey.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption e) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, e); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - Ready ready; - try { - ready = revokeKey.keys(System.in); - } catch (SOPGPException.KeyIsProtected e) { - String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", "STANDARD_IN"); - throw new SOPGPException.KeyIsProtected(errorMsg, e); - } - try { - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} 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 new file mode 100644 index 0000000..0b93ac5 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2023 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 sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.exception.SOPGPException.KeyIsProtected + +@Command( + name = "revoke-key", + resourceBundle = "msg_revoke-key", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class RevokeKeyCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: String? = null + + override fun run() { + val revokeKey = throwIfUnsupportedSubcommand(SopCLI.getSop().revokeKey(), "revoke-key") + + if (!armor) { + revokeKey.noArmor() + } + + withKeyPassword?.let { + try { + val password = stringFromInputStream(getInput(it)) + revokeKey.withKeyPassword(password) + } catch (e: SOPGPException.UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw SOPGPException.UnsupportedOption(errorMsg, e) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + val ready = + try { + revokeKey.keys(System.`in`) + } catch (e: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", "STANDARD_IN") + throw KeyIsProtected(errorMsg, e) + } + try { + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 377a7287b3a8fc2e26eba4608964844eac63b2de Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 17:34:30 +0100 Subject: [PATCH 115/298] Kotlin conversion: ArmorCmd --- .../sop/cli/picocli/commands/ArmorCmd.java | 50 ------------------- .../sop/cli/picocli/commands/ArmorCmd.kt | 42 ++++++++++++++++ 2 files changed, 42 insertions(+), 50 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java deleted file mode 100644 index 5691686..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; -import sop.operation.Armor; - -import java.io.IOException; - -@CommandLine.Command(name = "armor", - resourceBundle = "msg_armor", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ArmorCmd extends AbstractSopCmd { - - @CommandLine.Option(names = {"--label"}, - paramLabel = "{auto|sig|key|cert|message}") - ArmorLabel label; - - @Override - public void run() { - Armor armor = throwIfUnsupportedSubcommand( - SopCLI.getSop().armor(), - "armor"); - - if (label != null) { - try { - armor.label(label); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--label"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - try { - Ready ready = armor.data(System.in); - ready.writeTo(System.out); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data"); - throw new SOPGPException.BadData(errorMsg, badData); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt new file mode 100644 index 0000000..cb6bc79 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt @@ -0,0 +1,42 @@ +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.enums.ArmorLabel +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.UnsupportedOption + +@Command( + name = "armor", + resourceBundle = "msg_armor", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class ArmorCmd : AbstractSopCmd() { + + @Option(names = ["--label"], paramLabel = "{auto|sig|key|cert|message}") + var label: ArmorLabel? = null + + override fun run() { + val armor = throwIfUnsupportedSubcommand(SopCLI.getSop().armor(), "armor") + + label?.let { + try { + armor.label(it) + } catch (unsupported: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--label") + throw UnsupportedOption(errorMsg, unsupported) + } + } + + try { + val ready = armor.data(System.`in`) + ready.writeTo(System.out) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data") + throw BadData(errorMsg, badData) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 49120c5da883dca98ec243577b9368c9569dd858 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 17:41:07 +0100 Subject: [PATCH 116/298] Kotlin conversion: ChangeKeyPasswordCmd --- .../commands/ChangeKeyPasswordCmd.java | 56 ------------------- .../picocli/commands/ChangeKeyPasswordCmd.kt | 46 +++++++++++++++ 2 files changed, 46 insertions(+), 56 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java deleted file mode 100644 index 5a6aa2a..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.ChangeKeyPassword; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "change-key-password", - resourceBundle = "msg_change-key-password", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ChangeKeyPasswordCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = {"--old-key-password"}, - paramLabel = "PASSWORD") - List oldKeyPasswords = new ArrayList<>(); - - @CommandLine.Option(names = {"--new-key-password"}, arity = "0..1", - paramLabel = "PASSWORD") - String newKeyPassword = null; - - @Override - public void run() { - ChangeKeyPassword changeKeyPassword = throwIfUnsupportedSubcommand( - SopCLI.getSop().changeKeyPassword(), "change-key-password"); - - if (!armor) { - changeKeyPassword.noArmor(); - } - - for (String oldKeyPassword : oldKeyPasswords) { - changeKeyPassword.oldKeyPassphrase(oldKeyPassword); - } - - if (newKeyPassword != null) { - changeKeyPassword.newKeyPassphrase(newKeyPassword); - } - - try { - changeKeyPassword.keys(System.in).writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} 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 new file mode 100644 index 0000000..0c2eb4a --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import java.lang.RuntimeException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException + +@Command( + name = "change-key-password", + resourceBundle = "msg_change-key-password", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class ChangeKeyPasswordCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true + + @Option(names = ["--old-key-password"], paramLabel = "PASSWORD") + var oldKeyPasswords: List = listOf() + + @Option(names = ["--new-key-password"], arity = "0..1", paramLabel = "PASSWORD") + var newKeyPassword: String? = null + + override fun run() { + val changeKeyPassword = + throwIfUnsupportedSubcommand(SopCLI.getSop().changeKeyPassword(), "change-key-password") + + if (!armor) { + changeKeyPassword.noArmor() + } + + oldKeyPasswords.forEach { changeKeyPassword.oldKeyPassphrase(it) } + + newKeyPassword?.let { changeKeyPassword.newKeyPassphrase(it) } + + try { + changeKeyPassword.keys(System.`in`).writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 688b8043a26cce2a80807db136ababd656cb978a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 17:45:56 +0100 Subject: [PATCH 117/298] Kotlin conversion: DearmorCmd --- .../sop/cli/picocli/commands/DearmorCmd.java | 47 ------------------- .../sop/cli/picocli/commands/DearmorCmd.kt | 37 +++++++++++++++ 2 files changed, 37 insertions(+), 47 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java deleted file mode 100644 index f73e351..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Dearmor; - -import java.io.IOException; - -@CommandLine.Command(name = "dearmor", - resourceBundle = "msg_dearmor", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class DearmorCmd extends AbstractSopCmd { - - @Override - public void run() { - Dearmor dearmor = throwIfUnsupportedSubcommand( - SopCLI.getSop().dearmor(), "dearmor"); - - try { - dearmor.data(System.in) - .writeTo(System.out); - } catch (SOPGPException.BadData e) { - String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data"); - throw new SOPGPException.BadData(errorMsg, e); - } catch (IOException e) { - String msg = e.getMessage(); - if (msg == null) { - throw new RuntimeException(e); - } - - String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data"); - if (msg.equals("invalid armor") || - msg.equals("invalid armor header") || - msg.equals("inconsistent line endings in headers") || - msg.startsWith("unable to decode base64 data")) { - throw new SOPGPException.BadData(errorMsg, e); - } - - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt new file mode 100644 index 0000000..9ae97c6 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt @@ -0,0 +1,37 @@ +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.exception.SOPGPException.BadData + +@Command( + name = "dearmor", + resourceBundle = "msg_dearmor", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class DearmorCmd : AbstractSopCmd() { + + override fun run() { + val dearmor = throwIfUnsupportedSubcommand(SopCLI.getSop().dearmor(), "dearmor") + + try { + dearmor.data(System.`in`).writeTo(System.out) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data") + throw BadData(errorMsg, badData) + } catch (e: IOException) { + e.message?.let { + val errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data") + if (it == "invalid armor" || + it == "invalid armor header" || + it == "inconsistent line endings in headers" || + it.startsWith("unable to decode base64 data")) { + throw BadData(errorMsg, e) + } + throw RuntimeException(e) + } + ?: throw RuntimeException(e) + } + } +} From 8e65771e3638ac57c7ef3befed334fbf48c0c3e3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 17:50:07 +0100 Subject: [PATCH 118/298] Kotlin conversion: ListProfilesCmd --- .../cli/picocli/commands/ListProfilesCmd.java | 36 ------------------- .../cli/picocli/commands/ListProfilesCmd.kt | 34 ++++++++++++++++++ 2 files changed, 34 insertions(+), 36 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java deleted file mode 100644 index 53ec024..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Profile; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.ListProfiles; - -@CommandLine.Command(name = "list-profiles", - resourceBundle = "msg_list-profiles", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ListProfilesCmd extends AbstractSopCmd { - - @CommandLine.Parameters(paramLabel = "COMMAND", arity = "1", descriptionKey = "subcommand") - String subcommand; - - @Override - public void run() { - ListProfiles listProfiles = throwIfUnsupportedSubcommand( - SopCLI.getSop().listProfiles(), "list-profiles"); - - try { - for (Profile profile : listProfiles.subcommand(subcommand)) { - Print.outln(profile.toString()); - } - } catch (SOPGPException.UnsupportedProfile e) { - String errorMsg = getMsg("sop.error.feature_support.subcommand_does_not_support_profiles", subcommand); - throw new SOPGPException.UnsupportedProfile(errorMsg, e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt new file mode 100644 index 0000000..b770e82 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import picocli.CommandLine.Command +import picocli.CommandLine.Parameters +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.exception.SOPGPException.UnsupportedProfile + +@Command( + name = "list-profiles", + resourceBundle = "msg_list-profiles", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class ListProfilesCmd : AbstractSopCmd() { + + @Parameters(paramLabel = "COMMAND", arity = "1", descriptionKey = "subcommand") + lateinit var subcommand: String + + override fun run() { + val listProfiles = + throwIfUnsupportedSubcommand(SopCLI.getSop().listProfiles(), "list-profiles") + + try { + listProfiles.subcommand(subcommand).forEach { println(it) } + } catch (e: UnsupportedProfile) { + val errorMsg = + getMsg("sop.error.feature_support.subcommand_does_not_support_profiles", subcommand) + throw UnsupportedProfile(errorMsg, e) + } + } +} From 9daabb758a559b48b24582cd5f1e776c29c29309 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 18:18:55 +0100 Subject: [PATCH 119/298] Kotlin conversion: DecryptCmd --- .../sop/cli/picocli/commands/DecryptCmd.java | 255 ------------------ .../sop/cli/picocli/commands/DecryptCmd.kt | 223 +++++++++++++++ .../cli/picocli/commands/DecryptCmdTest.java | 6 +- 3 files changed, 226 insertions(+), 258 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java deleted file mode 100644 index a681b4d..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java +++ /dev/null @@ -1,255 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.Verification; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Decrypt; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -@CommandLine.Command(name = "decrypt", - resourceBundle = "msg_decrypt", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class DecryptCmd extends AbstractSopCmd { - - private static final String OPT_SESSION_KEY_OUT = "--session-key-out"; - private static final String OPT_WITH_SESSION_KEY = "--with-session-key"; - private static final String OPT_WITH_PASSWORD = "--with-password"; - private static final String OPT_WITH_KEY_PASSWORD = "--with-key-password"; - private static final String OPT_VERIFICATIONS_OUT = "--verifications-out"; // see SOP-05 - private static final String OPT_VERIFY_WITH = "--verify-with"; - private static final String OPT_NOT_BEFORE = "--verify-not-before"; - private static final String OPT_NOT_AFTER = "--verify-not-after"; - - - @CommandLine.Option( - names = {OPT_SESSION_KEY_OUT}, - paramLabel = "SESSIONKEY") - String sessionKeyOut; - - @CommandLine.Option( - names = {OPT_WITH_SESSION_KEY}, - paramLabel = "SESSIONKEY") - List withSessionKey = new ArrayList<>(); - - @CommandLine.Option( - names = {OPT_WITH_PASSWORD}, - paramLabel = "PASSWORD") - List withPassword = new ArrayList<>(); - - @CommandLine.Option(names = {OPT_VERIFICATIONS_OUT, "--verify-out"}, // TODO: Remove --verify-out in 06 - paramLabel = "VERIFICATIONS") - String verifyOut; - - @CommandLine.Option(names = {OPT_VERIFY_WITH}, - paramLabel = "CERT") - List certs = new ArrayList<>(); - - @CommandLine.Option(names = {OPT_NOT_BEFORE}, - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {OPT_NOT_AFTER}, - paramLabel = "DATE") - String notAfter = "now"; - - @CommandLine.Parameters(index = "0..*", - paramLabel = "KEY") - List keys = new ArrayList<>(); - - @CommandLine.Option(names = {OPT_WITH_KEY_PASSWORD}, - paramLabel = "PASSWORD") - List withKeyPassword = new ArrayList<>(); - - @Override - public void run() { - Decrypt decrypt = throwIfUnsupportedSubcommand( - SopCLI.getSop().decrypt(), "decrypt"); - - throwIfOutputExists(verifyOut); - throwIfOutputExists(sessionKeyOut); - - setNotAfter(notAfter, decrypt); - setNotBefore(notBefore, decrypt); - setWithPasswords(withPassword, decrypt); - setWithSessionKeys(withSessionKey, decrypt); - setWithKeyPassword(withKeyPassword, decrypt); - setVerifyWith(certs, decrypt); - setDecryptWith(keys, decrypt); - - if (verifyOut != null && certs.isEmpty()) { - String errorMsg = getMsg("sop.error.usage.option_requires_other_option", OPT_VERIFICATIONS_OUT, OPT_VERIFY_WITH); - throw new SOPGPException.IncompleteVerification(errorMsg); - } - - try { - ReadyWithResult ready = decrypt.ciphertext(System.in); - DecryptionResult result = ready.writeTo(System.out); - writeSessionKeyOut(result); - writeVerifyOut(result); - - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.stdin_not_a_message"); - throw new SOPGPException.BadData(errorMsg, badData); - } catch (SOPGPException.CannotDecrypt e) { - String errorMsg = getMsg("sop.error.runtime.cannot_decrypt_message"); - throw new SOPGPException.CannotDecrypt(errorMsg, e); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } - } - - private void writeVerifyOut(DecryptionResult result) throws IOException { - if (verifyOut != null) { - if (result.getVerifications().isEmpty()) { - String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found"); - throw new SOPGPException.NoSignature(errorMsg); - } - - try (OutputStream fileOut = getOutput(verifyOut)) { - PrintWriter writer = new PrintWriter(fileOut); - for (Verification verification : result.getVerifications()) { - // CHECKSTYLE:OFF - writer.println(verification.toString()); - // CHECKSTYLE:ON - } - writer.flush(); - } - } - } - - private void writeSessionKeyOut(DecryptionResult result) throws IOException { - if (sessionKeyOut == null) { - return; - } - try (OutputStream outputStream = getOutput(sessionKeyOut)) { - if (!result.getSessionKey().isPresent()) { - String errorMsg = getMsg("sop.error.runtime.no_session_key_extracted"); - throw new SOPGPException.UnsupportedOption(String.format(errorMsg, OPT_SESSION_KEY_OUT)); - } - SessionKey sessionKey = result.getSessionKey().get(); - PrintWriter writer = new PrintWriter(outputStream); - // CHECKSTYLE:OFF - writer.println(sessionKey.toString()); - // CHECKSTYLE:ON - writer.flush(); - } - } - - private void setDecryptWith(List keys, Decrypt decrypt) { - for (String key : keys) { - try (InputStream keyIn = getInput(key)) { - decrypt.withKey(keyIn); - } catch (SOPGPException.KeyIsProtected keyIsProtected) { - String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", key); - throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_private_key", key); - throw new SOPGPException.BadData(errorMsg, badData); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - private void setVerifyWith(List certs, Decrypt decrypt) { - for (String cert : certs) { - try (InputStream certIn = getInput(cert)) { - decrypt.verifyWithCert(certIn); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_certificate", cert); - throw new SOPGPException.BadData(errorMsg, badData); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } - } - } - - private void setWithSessionKeys(List withSessionKey, Decrypt decrypt) { - for (String sessionKeyFile : withSessionKey) { - String sessionKeyString; - try { - sessionKeyString = stringFromInputStream(getInput(sessionKeyFile)); - } catch (IOException e) { - throw new RuntimeException(e); - } - SessionKey sessionKey; - try { - sessionKey = SessionKey.fromString(sessionKeyString); - } catch (IllegalArgumentException e) { - String errorMsg = getMsg("sop.error.input.malformed_session_key"); - throw new IllegalArgumentException(errorMsg, e); - } - try { - decrypt.withSessionKey(sessionKey); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_SESSION_KEY); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - } - - private void setWithPasswords(List withPassword, Decrypt decrypt) { - for (String passwordFile : withPassword) { - try { - String password = stringFromInputStream(getInput(passwordFile)); - decrypt.withPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_PASSWORD); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - private void setWithKeyPassword(List withKeyPassword, Decrypt decrypt) { - for (String passwordFile : withKeyPassword) { - try { - String password = stringFromInputStream(getInput(passwordFile)); - decrypt.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_KEY_PASSWORD); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - private void setNotAfter(String notAfter, Decrypt decrypt) { - Date notAfterDate = parseNotAfter(notAfter); - try { - decrypt.verifyNotAfter(notAfterDate); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_AFTER); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - private void setNotBefore(String notBefore, Decrypt decrypt) { - Date notBeforeDate = parseNotBefore(notBefore); - try { - decrypt.verifyNotBefore(notBeforeDate); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_BEFORE); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } -} 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 new file mode 100644 index 0000000..95298c2 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt @@ -0,0 +1,223 @@ +package sop.cli.picocli.commands + +import java.io.IOException +import java.io.PrintWriter +import picocli.CommandLine.* +import sop.DecryptionResult +import sop.SessionKey +import sop.SessionKey.Companion.fromString +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.* +import sop.operation.Decrypt + +@Command( + name = "decrypt", + resourceBundle = "msg_decrypt", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class DecryptCmd : AbstractSopCmd() { + + @Option(names = [OPT_SESSION_KEY_OUT], paramLabel = "SESSIONKEY") + var sessionKeyOut: String? = null + + @Option(names = [OPT_WITH_SESSION_KEY], paramLabel = "SESSIONKEY") + var withSessionKey: List = listOf() + + @Option(names = [OPT_WITH_PASSWORD], paramLabel = "PASSWORD") + var withPassword: List = listOf() + + @Option(names = [OPT_VERIFICATIONS_OUT], paramLabel = "VERIFICATIONS") + var verifyOut: String? = null + + @Option(names = [OPT_VERIFY_WITH], paramLabel = "CERT") var certs: List = listOf() + + @Option(names = [OPT_NOT_BEFORE], paramLabel = "DATE") var notBefore = "-" + + @Option(names = [OPT_NOT_AFTER], paramLabel = "DATE") var notAfter = "now" + + @Parameters(index = "0..*", paramLabel = "KEY") var keys: List = listOf() + + @Option(names = [OPT_WITH_KEY_PASSWORD], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + override fun run() { + val decrypt = throwIfUnsupportedSubcommand(SopCLI.getSop().decrypt(), "decrypt") + + throwIfOutputExists(verifyOut) + throwIfOutputExists(sessionKeyOut) + + setNotAfter(notAfter, decrypt) + setNotBefore(notBefore, decrypt) + setWithPasswords(withPassword, decrypt) + setWithSessionKeys(withSessionKey, decrypt) + setWithKeyPassword(withKeyPassword, decrypt) + setVerifyWith(certs, decrypt) + setDecryptWith(keys, decrypt) + + if (verifyOut != null && certs.isEmpty()) { + val errorMsg = + getMsg( + "sop.error.usage.option_requires_other_option", + OPT_VERIFICATIONS_OUT, + OPT_VERIFY_WITH) + throw IncompleteVerification(errorMsg) + } + + try { + val ready = decrypt.ciphertext(System.`in`) + val result = ready.writeTo(System.out) + writeSessionKeyOut(result) + writeVerifyOut(result) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_a_message") + throw BadData(errorMsg, badData) + } catch (e: CannotDecrypt) { + val errorMsg = getMsg("sop.error.runtime.cannot_decrypt_message") + throw CannotDecrypt(errorMsg, e) + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } + } + + @Throws(IOException::class) + private fun writeVerifyOut(result: DecryptionResult) { + verifyOut?.let { + if (result.verifications.isEmpty()) { + val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found") + throw NoSignature(errorMsg) + } + + getOutput(verifyOut).use { out -> + PrintWriter(out).use { pw -> result.verifications.forEach { pw.println(it) } } + } + } + } + + @Throws(IOException::class) + private fun writeSessionKeyOut(result: DecryptionResult) { + sessionKeyOut?.let { fileName -> + getOutput(fileName).use { out -> + if (!result.sessionKey.isPresent) { + val errorMsg = getMsg("sop.error.runtime.no_session_key_extracted") + throw UnsupportedOption(String.format(errorMsg, OPT_SESSION_KEY_OUT)) + } + + PrintWriter(out).use { it.println(result.sessionKey.get()!!) } + } + } + } + + private fun setDecryptWith(keys: List, decrypt: Decrypt) { + for (key in keys) { + try { + getInput(key).use { decrypt.withKey(it) } + } catch (keyIsProtected: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", key) + throw KeyIsProtected(errorMsg, keyIsProtected) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", key) + throw BadData(errorMsg, badData) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + } + + private fun setVerifyWith(certs: List, decrypt: Decrypt) { + for (cert in certs) { + try { + getInput(cert).use { certIn -> decrypt.verifyWithCert(certIn) } + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", cert) + throw BadData(errorMsg, badData) + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } + } + } + + private fun setWithSessionKeys(withSessionKey: List, decrypt: Decrypt) { + for (sessionKeyFile in withSessionKey) { + val sessionKeyString: String = + try { + stringFromInputStream(getInput(sessionKeyFile)) + } catch (e: IOException) { + throw RuntimeException(e) + } + val sessionKey: SessionKey = + try { + fromString(sessionKeyString) + } catch (e: IllegalArgumentException) { + val errorMsg = getMsg("sop.error.input.malformed_session_key") + throw IllegalArgumentException(errorMsg, e) + } + try { + decrypt.withSessionKey(sessionKey) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_SESSION_KEY) + throw UnsupportedOption(errorMsg, unsupportedOption) + } + } + } + + private fun setWithPasswords(withPassword: List, decrypt: Decrypt) { + for (passwordFile in withPassword) { + try { + val password = stringFromInputStream(getInput(passwordFile)) + decrypt.withPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_PASSWORD) + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + } + + private fun setWithKeyPassword(withKeyPassword: List, decrypt: Decrypt) { + for (passwordFile in withKeyPassword) { + try { + val password = stringFromInputStream(getInput(passwordFile)) + decrypt.withKeyPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_KEY_PASSWORD) + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + } + + private fun setNotAfter(notAfter: String, decrypt: Decrypt) { + val notAfterDate = parseNotAfter(notAfter) + try { + decrypt.verifyNotAfter(notAfterDate) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_AFTER) + throw UnsupportedOption(errorMsg, unsupportedOption) + } + } + + private fun setNotBefore(notBefore: String, decrypt: Decrypt) { + val notBeforeDate = parseNotBefore(notBefore) + try { + decrypt.verifyNotBefore(notBeforeDate) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_BEFORE) + throw UnsupportedOption(errorMsg, unsupportedOption) + } + } + + companion object { + const val OPT_SESSION_KEY_OUT = "--session-key-out" + const val OPT_WITH_SESSION_KEY = "--with-session-key" + const val OPT_WITH_PASSWORD = "--with-password" + const val OPT_WITH_KEY_PASSWORD = "--with-key-password" + const val OPT_VERIFICATIONS_OUT = "--verifications-out" + const val OPT_VERIFY_WITH = "--verify-with" + const val OPT_NOT_BEFORE = "--verify-not-before" + const val OPT_NOT_AFTER = "--verify-not-after" + } +} 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 e3dd198..c2c67ba 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 @@ -278,7 +278,7 @@ public class DecryptCmdTest { File certFile = File.createTempFile("existing-verify-out-cert", ".asc"); File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp"); - SopCLI.main(new String[] {"decrypt", "--verify-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); + SopCLI.main(new String[] {"decrypt", "--verifications-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); } @Test @@ -302,7 +302,7 @@ public class DecryptCmdTest { } }); - SopCLI.main(new String[] {"decrypt", "--verify-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); + SopCLI.main(new String[] {"decrypt", "--verifications-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) { String line = reader.readLine(); assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line); @@ -377,6 +377,6 @@ public class DecryptCmdTest { @Test @ExpectSystemExitWithStatus(SOPGPException.IncompleteVerification.EXIT_CODE) public void verifyOutWithoutVerifyWithCausesExit23() { - SopCLI.main(new String[] {"decrypt", "--verify-out", "out.file"}); + SopCLI.main(new String[] {"decrypt", "--verifications-out", "out.file"}); } } From 714c933cef399b3c98101c51b14d4f86e45fb407 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 18:31:52 +0100 Subject: [PATCH 120/298] Kotlin conversion: InlineDetachCmd --- .../cli/picocli/commands/InlineDetachCmd.java | 50 ------------------- .../cli/picocli/commands/InlineDetachCmd.kt | 47 +++++++++++++++++ 2 files changed, 47 insertions(+), 50 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java deleted file mode 100644 index 52b654f..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Signatures; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.InlineDetach; - -import java.io.IOException; -import java.io.OutputStream; - -@CommandLine.Command(name = "inline-detach", - resourceBundle = "msg_inline-detach", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class InlineDetachCmd extends AbstractSopCmd { - - @CommandLine.Option( - names = {"--signatures-out"}, - paramLabel = "SIGNATURES") - String signaturesOut; - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @Override - public void run() { - InlineDetach inlineDetach = throwIfUnsupportedSubcommand( - SopCLI.getSop().inlineDetach(), "inline-detach"); - - throwIfOutputExists(signaturesOut); - throwIfMissingArg(signaturesOut, "--signatures-out"); - - if (!armor) { - inlineDetach.noArmor(); - } - - try (OutputStream outputStream = getOutput(signaturesOut)) { - Signatures signatures = inlineDetach - .message(System.in).writeTo(System.out); - signatures.writeTo(outputStream); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt new file mode 100644 index 0000000..e311adf --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import java.lang.RuntimeException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException + +@Command( + name = "inline-detach", + resourceBundle = "msg_inline-detach", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class InlineDetachCmd : AbstractSopCmd() { + + @Option(names = ["--signatures-out"], paramLabel = "SIGNATURES") + var signaturesOut: String? = null + + @Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true + + override fun run() { + val inlineDetach = + throwIfUnsupportedSubcommand(SopCLI.getSop().inlineDetach(), "inline-detach") + + throwIfOutputExists(signaturesOut) + throwIfMissingArg(signaturesOut, "--signatures-out") + + if (!armor) { + inlineDetach.noArmor() + } + + try { + getOutput(signaturesOut).use { sigOut -> + inlineDetach + .message(System.`in`) + .writeTo(System.out) // message out + .writeTo(sigOut) // signatures out + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 159ffbe0846960f419ba743a744f000012adcd49 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 18:32:01 +0100 Subject: [PATCH 121/298] Add missing license headers --- .../src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt | 4 ++++ .../src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt | 4 ++++ .../src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt index cb6bc79..ccfba4d 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package sop.cli.picocli.commands import java.io.IOException diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt index 9ae97c6..09d2a71 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package sop.cli.picocli.commands import java.io.IOException 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 95298c2..1e688b3 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 @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package sop.cli.picocli.commands import java.io.IOException From bfad8c4203258563492323dd2bbbf2646a72e2de Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:21:42 +0100 Subject: [PATCH 122/298] Kotlin conversion: EncryptCmd --- .../sop/cli/picocli/commands/EncryptCmd.java | 154 ------------------ .../sop/cli/picocli/commands/EncryptCmd.kt | 138 ++++++++++++++++ 2 files changed, 138 insertions(+), 154 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java deleted file mode 100644 index efda26f..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.operation.Encrypt; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "encrypt", - resourceBundle = "msg_encrypt", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class EncryptCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = {"--as"}, - paramLabel = "{binary|text}") - EncryptAs type; - - @CommandLine.Option(names = "--with-password", - paramLabel = "PASSWORD") - List withPassword = new ArrayList<>(); - - @CommandLine.Option(names = "--sign-with", - paramLabel = "KEY") - List signWith = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - paramLabel = "PASSWORD") - List withKeyPassword = new ArrayList<>(); - - @CommandLine.Option(names = "--profile", - paramLabel = "PROFILE") - String profile; - - @CommandLine.Parameters(index = "0..*", - paramLabel = "CERTS") - List certs = new ArrayList<>(); - - @Override - public void run() { - Encrypt encrypt = throwIfUnsupportedSubcommand( - SopCLI.getSop().encrypt(), "encrypt"); - - if (profile != null) { - try { - encrypt.profile(profile); - } catch (SOPGPException.UnsupportedProfile e) { - String errorMsg = getMsg("sop.error.usage.profile_not_supported", "encrypt", profile); - throw new SOPGPException.UnsupportedProfile(errorMsg, e); - } - } - - if (type != null) { - try { - encrypt.mode(type); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - if (withPassword.isEmpty() && certs.isEmpty()) { - String errorMsg = getMsg("sop.error.usage.password_or_cert_required"); - throw new SOPGPException.MissingArg(errorMsg); - } - - for (String passwordFileName : withPassword) { - try { - String password = stringFromInputStream(getInput(passwordFileName)); - encrypt.withPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - for (String passwordFileName : withKeyPassword) { - try { - String password = stringFromInputStream(getInput(passwordFileName)); - encrypt.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - for (String keyInput : signWith) { - try (InputStream keyIn = getInput(keyInput)) { - encrypt.signWith(keyIn); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.KeyIsProtected keyIsProtected) { - String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput); - throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - String errorMsg = getMsg("sop.error.runtime.key_uses_unsupported_asymmetric_algorithm", keyInput); - throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo); - } catch (SOPGPException.KeyCannotSign keyCannotSign) { - String errorMsg = getMsg("sop.error.runtime.key_cannot_sign", keyInput); - throw new SOPGPException.KeyCannotSign(errorMsg, keyCannotSign); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - for (String certInput : certs) { - try (InputStream certIn = getInput(certInput)) { - encrypt.withCert(certIn); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - String errorMsg = getMsg("sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput); - throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo); - } catch (SOPGPException.CertCannotEncrypt certCannotEncrypt) { - String errorMsg = getMsg("sop.error.runtime.cert_cannot_encrypt", certInput); - throw new SOPGPException.CertCannotEncrypt(errorMsg, certCannotEncrypt); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - if (!armor) { - encrypt.noArmor(); - } - - try { - Ready ready = encrypt.plaintext(System.in); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt new file mode 100644 index 0000000..b3b0d87 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.enums.EncryptAs +import sop.exception.SOPGPException.* + +@Command( + name = "encrypt", + resourceBundle = "msg_encrypt", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class EncryptCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--as"], paramLabel = "{binary|text}") var type: EncryptAs? = null + + @Option(names = ["--with-password"], paramLabel = "PASSWORD") + var withPassword: List = listOf() + + @Option(names = ["--sign-with"], paramLabel = "KEY") var signWith: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + @Option(names = ["--profile"], paramLabel = "PROFILE") var profile: String? = null + + @Parameters(index = "0..*", paramLabel = "CERTS") var certs: List = listOf() + + override fun run() { + val encrypt = throwIfUnsupportedSubcommand(SopCLI.getSop().encrypt(), "encrypt") + + profile?.let { + try { + encrypt.profile(it) + } catch (e: UnsupportedProfile) { + val errorMsg = getMsg("sop.error.usage.profile_not_supported", "encrypt", it) + throw UnsupportedProfile(errorMsg, e) + } + } + + type?.let { + try { + encrypt.mode(it) + } catch (e: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as") + throw UnsupportedOption(errorMsg, e) + } + } + + if (withPassword.isEmpty() && certs.isEmpty()) { + val errorMsg = getMsg("sop.error.usage.password_or_cert_required") + throw MissingArg(errorMsg) + } + + for (passwordFileName in withPassword) { + try { + val password = stringFromInputStream(getInput(passwordFileName)) + encrypt.withPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-password") + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + for (passwordFileName in withKeyPassword) { + try { + val password = stringFromInputStream(getInput(passwordFileName)) + encrypt.withKeyPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + for (keyInput in signWith) { + try { + getInput(keyInput).use { keyIn -> encrypt.signWith(keyIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (keyIsProtected: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput) + throw KeyIsProtected(errorMsg, keyIsProtected) + } catch (unsupportedAsymmetricAlgo: UnsupportedAsymmetricAlgo) { + val errorMsg = + getMsg("sop.error.runtime.key_uses_unsupported_asymmetric_algorithm", keyInput) + throw UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo) + } catch (keyCannotSign: KeyCannotSign) { + val errorMsg = getMsg("sop.error.runtime.key_cannot_sign", keyInput) + throw KeyCannotSign(errorMsg, keyCannotSign) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput) + throw BadData(errorMsg, badData) + } + } + + for (certInput in certs) { + try { + getInput(certInput).use { certIn -> encrypt.withCert(certIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (unsupportedAsymmetricAlgo: UnsupportedAsymmetricAlgo) { + val errorMsg = + getMsg( + "sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput) + throw UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo) + } catch (certCannotEncrypt: CertCannotEncrypt) { + val errorMsg = getMsg("sop.error.runtime.cert_cannot_encrypt", certInput) + throw CertCannotEncrypt(errorMsg, certCannotEncrypt) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput) + throw BadData(errorMsg, badData) + } + } + + if (!armor) { + encrypt.noArmor() + } + + try { + val ready = encrypt.plaintext(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 019dd63e1b6d4ee56d4d9c02328310ff8d213d16 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:26:06 +0100 Subject: [PATCH 123/298] Kotlin conversion: ExtractCertCmd --- .../cli/picocli/commands/ExtractCertCmd.java | 43 ------------------- .../cli/picocli/commands/ExtractCertCmd.kt | 40 +++++++++++++++++ 2 files changed, 40 insertions(+), 43 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ExtractCertCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java deleted file mode 100644 index 64a7a84..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.IOException; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.ExtractCert; - -@CommandLine.Command(name = "extract-cert", - resourceBundle = "msg_extract-cert", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ExtractCertCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @Override - public void run() { - ExtractCert extractCert = throwIfUnsupportedSubcommand( - SopCLI.getSop().extractCert(), "extract-cert"); - - if (!armor) { - extractCert.noArmor(); - } - - try { - Ready ready = extractCert.key(System.in); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.stdin_not_a_private_key"); - throw new SOPGPException.BadData(errorMsg, badData); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ExtractCertCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ExtractCertCmd.kt new file mode 100644 index 0000000..cff996f --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ExtractCertCmd.kt @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 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 sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.exception.SOPGPException.BadData + +@Command( + name = "extract-cert", + resourceBundle = "msg_extract-cert", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class ExtractCertCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + override fun run() { + val extractCert = + throwIfUnsupportedSubcommand(SopCLI.getSop().extractCert(), "extract-cert") + + if (!armor) { + extractCert.noArmor() + } + + try { + val ready = extractCert.key(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_a_private_key") + throw BadData(errorMsg, badData) + } + } +} From e9a5467f6b885b44248ee6b4555e3f991769d9b3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:33:34 +0100 Subject: [PATCH 124/298] Kotlin conversion: GenerateKeyCmd --- .../cli/picocli/commands/GenerateKeyCmd.java | 85 ------------------- .../cli/picocli/commands/GenerateKeyCmd.kt | 76 +++++++++++++++++ 2 files changed, 76 insertions(+), 85 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java deleted file mode 100644 index eea992e..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.GenerateKey; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "generate-key", - resourceBundle = "msg_generate-key", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class GenerateKeyCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Parameters(paramLabel = "USERID") - List userId = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - paramLabel = "PASSWORD") - String withKeyPassword; - - @CommandLine.Option(names = "--profile", - paramLabel = "PROFILE") - String profile; - - @CommandLine.Option(names = "--signing-only") - boolean signingOnly = false; - - @Override - public void run() { - GenerateKey generateKey = throwIfUnsupportedSubcommand( - SopCLI.getSop().generateKey(), "generate-key"); - - if (profile != null) { - try { - generateKey.profile(profile); - } catch (SOPGPException.UnsupportedProfile e) { - String errorMsg = getMsg("sop.error.usage.profile_not_supported", "generate-key", profile); - throw new SOPGPException.UnsupportedProfile(errorMsg, e); - } - } - - if (signingOnly) { - generateKey.signingOnly(); - } - - for (String userId : userId) { - generateKey.userId(userId); - } - - if (!armor) { - generateKey.noArmor(); - } - - if (withKeyPassword != null) { - try { - String password = stringFromInputStream(getInput(withKeyPassword)); - generateKey.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption e) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, e); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - try { - Ready ready = generateKey.generate(); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt new file mode 100644 index 0000000..fb5e321 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.UnsupportedOption +import sop.exception.SOPGPException.UnsupportedProfile + +@Command( + name = "generate-key", + resourceBundle = "msg_generate-key", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class GenerateKeyCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Parameters(paramLabel = "USERID") var userId: List = listOf() + + @Option(names = ["---with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: String? = null + + @Option(names = ["--profile"], paramLabel = "PROFILE") var profile: String? = null + + @Option(names = ["--signing-only"]) var signingOnly: Boolean = false + + override fun run() { + val generateKey = + throwIfUnsupportedSubcommand(SopCLI.getSop().generateKey(), "generate-key") + + profile?.let { + try { + generateKey.profile(it) + } catch (e: UnsupportedProfile) { + val errorMsg = + getMsg("sop.error.usage.profile_not_supported", "generate-key", profile!!) + throw UnsupportedProfile(errorMsg, e) + } + } + + if (signingOnly) { + generateKey.signingOnly() + } + + for (userId in userId) { + generateKey.userId(userId) + } + + if (!armor) { + generateKey.noArmor() + } + + withKeyPassword?.let { + try { + val password = stringFromInputStream(getInput(it)) + generateKey.withKeyPassword(password) + } catch (e: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw UnsupportedOption(errorMsg, e) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + try { + val ready = generateKey.generate() + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 2e118357e2cee22e65ef999e94c830d70e48ad54 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:40:46 +0100 Subject: [PATCH 125/298] Kotlin conversion: InlineSignCmd --- .../cli/picocli/commands/InlineSignCmd.java | 101 ------------------ .../sop/cli/picocli/commands/InlineSignCmd.kt | 89 +++++++++++++++ 2 files changed, 89 insertions(+), 101 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java deleted file mode 100644 index 1865bcf..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.enums.InlineSignAs; -import sop.exception.SOPGPException; -import sop.operation.InlineSign; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "inline-sign", - resourceBundle = "msg_inline-sign", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class InlineSignCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = "--as", - paramLabel = "{binary|text|clearsigned}") - InlineSignAs type; - - @CommandLine.Parameters(paramLabel = "KEYS") - List secretKeyFile = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - paramLabel = "PASSWORD") - List withKeyPassword = new ArrayList<>(); - - @Override - public void run() { - InlineSign inlineSign = throwIfUnsupportedSubcommand( - SopCLI.getSop().inlineSign(), "inline-sign"); - - // Clearsigned messages are inherently armored, so --no-armor makes no sense. - if (!armor && type == InlineSignAs.clearsigned) { - String errorMsg = getMsg("sop.error.usage.incompatible_options.clearsigned_no_armor"); - throw new SOPGPException.IncompatibleOptions(errorMsg); - } - - if (type != null) { - try { - inlineSign.mode(type); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - if (secretKeyFile.isEmpty()) { - String errorMsg = getMsg("sop.error.usage.parameter_required", "KEYS"); - throw new SOPGPException.MissingArg(errorMsg); - } - - for (String passwordFile : withKeyPassword) { - try { - String password = stringFromInputStream(getInput(passwordFile)); - inlineSign.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - for (String keyInput : secretKeyFile) { - try (InputStream keyIn = getInput(keyInput)) { - inlineSign.key(keyIn); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.KeyIsProtected e) { - String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput); - throw new SOPGPException.KeyIsProtected(errorMsg, e); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - if (!armor) { - inlineSign.noArmor(); - } - - try { - Ready ready = inlineSign.data(System.in); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt new file mode 100644 index 0000000..c41f6f6 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.enums.InlineSignAs +import sop.exception.SOPGPException.* + +@Command( + name = "inline-sign", + resourceBundle = "msg_inline-sign", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class InlineSignCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--as"], paramLabel = "{binary|text|clearsigned}") + var type: InlineSignAs? = null + + @Parameters(paramLabel = "KEYS") var secretKeyFile: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + override fun run() { + val inlineSign = throwIfUnsupportedSubcommand(SopCLI.getSop().inlineSign(), "inline-sign") + + if (!armor && type == InlineSignAs.clearsigned) { + val errorMsg = getMsg("sop.error.usage.incompatible_options.clearsigned_no_armor") + throw IncompatibleOptions(errorMsg) + } + + type?.let { + try { + inlineSign.mode(it) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + } + + if (secretKeyFile.isEmpty()) { + val errorMsg = getMsg("sop.error.usage.parameter_required", "KEYS") + throw MissingArg(errorMsg) + } + + for (passwordFile in withKeyPassword) { + try { + val password = stringFromInputStream(getInput(passwordFile)) + inlineSign.withKeyPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + for (keyInput in secretKeyFile) { + try { + getInput(keyInput).use { keyIn -> inlineSign.key(keyIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (e: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput) + throw KeyIsProtected(errorMsg, e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput) + throw BadData(errorMsg, badData) + } + } + + if (!armor) { + inlineSign.noArmor() + } + + try { + val ready = inlineSign.data(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From b884f2b1a9447064d17f6944d1dcaf0fcd43c3c8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:49:04 +0100 Subject: [PATCH 126/298] Kotlin conversion: InlineVerifyCmd --- .../cli/picocli/commands/InlineVerifyCmd.java | 108 ------------------ .../cli/picocli/commands/package-info.java | 8 -- .../cli/picocli/commands/InlineVerifyCmd.kt | 93 +++++++++++++++ 3 files changed, 93 insertions(+), 116 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineVerifyCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java deleted file mode 100644 index c413c85..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.ReadyWithResult; -import sop.Verification; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.InlineVerify; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "inline-verify", - resourceBundle = "msg_inline-verify", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class InlineVerifyCmd extends AbstractSopCmd { - - @CommandLine.Parameters(arity = "0..*", - paramLabel = "CERT") - List certificates = new ArrayList<>(); - - @CommandLine.Option(names = {"--not-before"}, - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {"--not-after"}, - paramLabel = "DATE") - String notAfter = "now"; - - @CommandLine.Option(names = "--verifications-out", paramLabel = "VERIFICATIONS") - String verificationsOut; - - @Override - public void run() { - InlineVerify inlineVerify = throwIfUnsupportedSubcommand( - SopCLI.getSop().inlineVerify(), "inline-verify"); - - throwIfOutputExists(verificationsOut); - - if (notAfter != null) { - try { - inlineVerify.notAfter(parseNotAfter(notAfter)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - if (notBefore != null) { - try { - inlineVerify.notBefore(parseNotBefore(notBefore)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - for (String certInput : certificates) { - try (InputStream certIn = getInput(certInput)) { - inlineVerify.cert(certIn); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - String errorMsg = getMsg("sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput); - throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - List verifications = null; - try { - ReadyWithResult> ready = inlineVerify.data(System.in); - verifications = ready.writeTo(System.out); - } catch (SOPGPException.NoSignature e) { - String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found"); - throw new SOPGPException.NoSignature(errorMsg, e); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.stdin_not_a_message"); - throw new SOPGPException.BadData(errorMsg, badData); - } - - if (verificationsOut != null) { - try (OutputStream outputStream = getOutput(verificationsOut)) { - PrintWriter pw = new PrintWriter(outputStream); - for (Verification verification : verifications) { - // CHECKSTYLE:OFF - pw.println(verification); - // CHECKSTYLE:ON - } - pw.flush(); - pw.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java deleted file mode 100644 index fc6aefd..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Subcommands of the PGPainless SOP. - */ -package sop.cli.picocli.commands; diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineVerifyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineVerifyCmd.kt new file mode 100644 index 0000000..6a641a6 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineVerifyCmd.kt @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import java.io.PrintWriter +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.* + +@Command( + name = "inline-verify", + resourceBundle = "msg_inline-verify", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class InlineVerifyCmd : AbstractSopCmd() { + + @Parameters(arity = "0..*", paramLabel = "CERT") var certificates: List = listOf() + + @Option(names = ["--not-before"], paramLabel = "DATE") var notBefore: String = "-" + + @Option(names = ["--not-after"], paramLabel = "DATE") var notAfter: String = "now" + + @Option(names = ["--verifications-out"], paramLabel = "VERIFICATIONS") + var verificationsOut: String? = null + + override fun run() { + val inlineVerify = + throwIfUnsupportedSubcommand(SopCLI.getSop().inlineVerify(), "inline-verify") + + throwIfOutputExists(verificationsOut) + + try { + inlineVerify.notAfter(parseNotAfter(notAfter)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + try { + inlineVerify.notBefore(parseNotBefore(notBefore)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + for (certInput in certificates) { + try { + getInput(certInput).use { certIn -> inlineVerify.cert(certIn) } + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (unsupportedAsymmetricAlgo: UnsupportedAsymmetricAlgo) { + val errorMsg = + getMsg( + "sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput) + throw UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput) + throw BadData(errorMsg, badData) + } + } + + val verifications = + try { + val ready = inlineVerify.data(System.`in`) + ready.writeTo(System.out) + } catch (e: NoSignature) { + val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found") + throw NoSignature(errorMsg, e) + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_a_message") + throw BadData(errorMsg, badData) + } + + verificationsOut?.let { + try { + getOutput(it).use { outputStream -> + val pw = PrintWriter(outputStream) + for (verification in verifications) { + pw.println(verification) + } + pw.flush() + pw.close() + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + } +} From b251956f492268eadb7f1876e671a34b87c38b9e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:49:45 +0100 Subject: [PATCH 127/298] Delete unused Print class --- .../src/main/java/sop/cli/picocli/Print.java | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/Print.java diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java b/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java deleted file mode 100644 index 9e81b66..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -public class Print { - - public static void outln(String string) { - // CHECKSTYLE:OFF - System.out.println(string); - // CHECKSTYLE:ON - } -} From 5c2695228b6e780d6cbcb9959c580732c76a3af2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:55:50 +0100 Subject: [PATCH 128/298] Kotlin conversion: SOPExceptionExitCodeMapper --- .../picocli/SOPExceptionExitCodeMapper.java | 34 ------------------- .../cli/picocli/SOPExceptionExitCodeMapper.kt | 31 +++++++++++++++++ 2 files changed, 31 insertions(+), 34 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java deleted file mode 100644 index 8b38af3..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.CommandLine; -import sop.exception.SOPGPException; - -public class SOPExceptionExitCodeMapper implements CommandLine.IExitCodeExceptionMapper { - - @Override - public int getExitCode(Throwable exception) { - if (exception instanceof SOPGPException) { - return ((SOPGPException) exception).getExitCode(); - } - if (exception instanceof CommandLine.UnmatchedArgumentException) { - CommandLine.UnmatchedArgumentException ex = (CommandLine.UnmatchedArgumentException) exception; - // Unmatched option of subcommand (eg. `generate-key -k`) - if (ex.isUnknownOption()) { - return SOPGPException.UnsupportedOption.EXIT_CODE; - } - // Unmatched subcommand - return SOPGPException.UnsupportedSubcommand.EXIT_CODE; - } - // Invalid option (eg. `--label Invalid`) - if (exception instanceof CommandLine.ParameterException) { - return SOPGPException.UnsupportedOption.EXIT_CODE; - } - - // Others, like IOException etc. - return 1; - } -} 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 new file mode 100644 index 0000000..29aa77b --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli + +import picocli.CommandLine.* +import sop.exception.SOPGPException + +class SOPExceptionExitCodeMapper : IExitCodeExceptionMapper { + + override fun getExitCode(exception: Throwable): Int = + if (exception is SOPGPException) { + // SOPGPExceptions have well-defined exit code + exception.getExitCode() + } else if (exception is UnmatchedArgumentException) { + if (exception.isUnknownOption) { + // Unmatched option of subcommand (e.g. `generate-key --unknown`) + SOPGPException.UnsupportedOption.EXIT_CODE + } else { + // Unmatched subcommand + SOPGPException.UnsupportedSubcommand.EXIT_CODE + } + } else if (exception is ParameterException) { + // Invalid option (e.g. `--as invalid`) + SOPGPException.UnsupportedOption.EXIT_CODE + } else { + // Others, like IOException etc. + 1 + } +} From 0c2cf5cb19dec00e9c9de41ffb7307fd859fc113 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:59:11 +0100 Subject: [PATCH 129/298] Kotlin conversion: SOPExecutionExceptionHandler --- .../picocli/SOPExecutionExceptionHandler.java | 33 ------------------ .../picocli/SOPExecutionExceptionHandler.kt | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 33 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExecutionExceptionHandler.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java deleted file mode 100644 index f6906ff..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.CommandLine; - -public class SOPExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler { - - @Override - public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) { - - int exitCode = commandLine.getExitCodeExceptionMapper() != null ? - commandLine.getExitCodeExceptionMapper().getExitCode(ex) : - commandLine.getCommandSpec().exitCodeOnExecutionException(); - - CommandLine.Help.ColorScheme colorScheme = commandLine.getColorScheme(); - // CHECKSTYLE:OFF - if (ex.getMessage() != null) { - commandLine.getErr().println(colorScheme.errorText(ex.getMessage())); - } else { - commandLine.getErr().println(ex.getClass().getName()); - } - - if (SopCLI.stacktrace) { - ex.printStackTrace(commandLine.getErr()); - } - // CHECKSTYLE:ON - - return exitCode; - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExecutionExceptionHandler.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExecutionExceptionHandler.kt new file mode 100644 index 0000000..52236d3 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExecutionExceptionHandler.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli + +import picocli.CommandLine +import picocli.CommandLine.IExecutionExceptionHandler + +class SOPExecutionExceptionHandler : IExecutionExceptionHandler { + override fun handleExecutionException( + ex: Exception, + commandLine: CommandLine, + parseResult: CommandLine.ParseResult + ): Int { + val exitCode = + if (commandLine.exitCodeExceptionMapper != null) + commandLine.exitCodeExceptionMapper.getExitCode(ex) + else commandLine.commandSpec.exitCodeOnExecutionException() + + val colorScheme = commandLine.colorScheme + if (ex.message != null) { + commandLine.getErr().println(colorScheme.errorText(ex.message)) + } else { + commandLine.getErr().println(ex.javaClass.getName()) + } + + if (SopCLI.stacktrace) { + ex.printStackTrace(commandLine.getErr()) + } + + return exitCode + } +} From baa44a6b1a4218a051ea8ee1045736224beea20f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 12:35:23 +0100 Subject: [PATCH 130/298] Kotlin conversion: SopCLI --- .../src/main/java/sop/cli/picocli/SopCLI.java | 129 ------------------ .../java/sop/cli/picocli/package-info.java | 8 -- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 113 +++++++++++++++ .../test/java/sop/cli/picocli/SOPTest.java | 2 +- 4 files changed, 114 insertions(+), 138 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java deleted file mode 100644 index 5420dea..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.AutoComplete; -import picocli.CommandLine; -import sop.SOP; -import sop.cli.picocli.commands.ArmorCmd; -import sop.cli.picocli.commands.ChangeKeyPasswordCmd; -import sop.cli.picocli.commands.DearmorCmd; -import sop.cli.picocli.commands.DecryptCmd; -import sop.cli.picocli.commands.InlineDetachCmd; -import sop.cli.picocli.commands.EncryptCmd; -import sop.cli.picocli.commands.ExtractCertCmd; -import sop.cli.picocli.commands.GenerateKeyCmd; -import sop.cli.picocli.commands.InlineSignCmd; -import sop.cli.picocli.commands.InlineVerifyCmd; -import sop.cli.picocli.commands.ListProfilesCmd; -import sop.cli.picocli.commands.RevokeKeyCmd; -import sop.cli.picocli.commands.SignCmd; -import sop.cli.picocli.commands.VerifyCmd; -import sop.cli.picocli.commands.VersionCmd; -import sop.exception.SOPGPException; - -import java.util.List; -import java.util.Locale; -import java.util.ResourceBundle; - -@CommandLine.Command( - name = "sop", - resourceBundle = "msg_sop", - exitCodeOnInvalidInput = SOPGPException.UnsupportedSubcommand.EXIT_CODE, - subcommands = { - // Meta Subcommands - VersionCmd.class, - ListProfilesCmd.class, - // Key and Certificate Management Subcommands - GenerateKeyCmd.class, - ChangeKeyPasswordCmd.class, - RevokeKeyCmd.class, - ExtractCertCmd.class, - // Messaging Subcommands - SignCmd.class, - VerifyCmd.class, - EncryptCmd.class, - DecryptCmd.class, - InlineDetachCmd.class, - InlineSignCmd.class, - InlineVerifyCmd.class, - // Transport Subcommands - ArmorCmd.class, - DearmorCmd.class, - // Miscellaneous Subcommands - CommandLine.HelpCommand.class, - AutoComplete.GenerateCompletion.class - } -) -public class SopCLI { - // Singleton - static SOP SOP_INSTANCE; - static ResourceBundle cliMsg = ResourceBundle.getBundle("msg_sop"); - - public static String EXECUTABLE_NAME = "sop"; - - @CommandLine.Option(names = {"--stacktrace"}, - scope = CommandLine.ScopeType.INHERIT) - static boolean stacktrace; - - public static void main(String[] args) { - int exitCode = execute(args); - if (exitCode != 0) { - System.exit(exitCode); - } - } - - public static int execute(String[] args) { - - // Set locale - new CommandLine(new InitLocale()).parseArgs(args); - - // get error message bundle - cliMsg = ResourceBundle.getBundle("msg_sop"); - - // Prepare CLI - CommandLine cmd = new CommandLine(SopCLI.class); - - // explicitly set help command resource bundle - cmd.getSubcommands().get("help").setResourceBundle(ResourceBundle.getBundle("msg_help")); - - // Hide generate-completion command - cmd.getSubcommands().get("generate-completion").getCommandSpec().usageMessage().hidden(true); - - cmd.setCommandName(EXECUTABLE_NAME) - .setExecutionExceptionHandler(new SOPExecutionExceptionHandler()) - .setExitCodeExceptionMapper(new SOPExceptionExitCodeMapper()) - .setCaseInsensitiveEnumValuesAllowed(true); - - return cmd.execute(args); - } - - public static SOP getSop() { - if (SOP_INSTANCE == null) { - String errorMsg = cliMsg.getString("sop.error.runtime.no_backend_set"); - throw new IllegalStateException(errorMsg); - } - return SOP_INSTANCE; - } - - public static void setSopInstance(SOP instance) { - SOP_INSTANCE = instance; - } -} - -/** - * Control the locale. - * - * @see Picocli Readme - */ -class InitLocale { - @CommandLine.Option(names = { "-l", "--locale" }, descriptionKey = "sop.locale") - void setLocale(String locale) { - Locale.setDefault(new Locale(locale)); - } - - @CommandLine.Unmatched - List remainder; // ignore any other parameters and options in the first parsing phase -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java deleted file mode 100644 index 83f426d..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Implementation of the Stateless OpenPGP Command Line Interface using Picocli. - */ -package sop.cli.picocli; 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 new file mode 100644 index 0000000..1d5d46b --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli + +import java.util.* +import kotlin.system.exitProcess +import picocli.AutoComplete.GenerateCompletion +import picocli.CommandLine +import picocli.CommandLine.* +import sop.SOP +import sop.cli.picocli.commands.* +import sop.exception.SOPGPException + +@Command( + name = "sop", + resourceBundle = "msg_sop", + exitCodeOnInvalidInput = SOPGPException.UnsupportedSubcommand.EXIT_CODE, + subcommands = + [ + // Meta subcommands + VersionCmd::class, + ListProfilesCmd::class, + // Key and certificate management + GenerateKeyCmd::class, + ChangeKeyPasswordCmd::class, + RevokeKeyCmd::class, + ExtractCertCmd::class, + // Messaging subcommands + SignCmd::class, + VerifyCmd::class, + EncryptCmd::class, + DecryptCmd::class, + InlineDetachCmd::class, + InlineSignCmd::class, + InlineVerifyCmd::class, + // Transport + ArmorCmd::class, + DearmorCmd::class, + // misc + HelpCommand::class, + GenerateCompletion::class]) +class SopCLI { + + companion object { + @JvmStatic private var sopInstance: SOP? = null + + @JvmStatic + fun getSop(): SOP = + checkNotNull(sopInstance) { cliMsg.getString("sop.error.runtime.no_backend_set") } + + @JvmStatic + fun setSopInstance(sop: SOP?) { + sopInstance = sop + } + + @JvmField var cliMsg: ResourceBundle = ResourceBundle.getBundle("msg_sop") + + @JvmField var EXECUTABLE_NAME = "sop" + + @JvmField + @Option(names = ["--stacktrace"], scope = CommandLine.ScopeType.INHERIT) + var stacktrace = false + + @JvmStatic + fun main(vararg args: String) { + val exitCode = execute(*args) + if (exitCode != 0) { + exitProcess(exitCode) + } + } + + @JvmStatic + fun execute(vararg args: String): Int { + // Set locale + CommandLine(InitLocale()).parseArgs(*args) + + // Re-set bundle with updated locale + cliMsg = ResourceBundle.getBundle("msg_sop") + + return CommandLine(SopCLI::class.java) + .apply { + // explicitly set help command resource bundle + subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help")) + // Hide generate-completion command + subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true) + // overwrite executable name + commandName = EXECUTABLE_NAME + // setup exception handling + executionExceptionHandler = SOPExecutionExceptionHandler() + exitCodeExceptionMapper = SOPExceptionExitCodeMapper() + isCaseInsensitiveEnumValuesAllowed = true + } + .execute(*args) + } + } + + /** + * Control the locale. + * + * @see Picocli Readme + */ + @Command + class InitLocale { + @Option(names = ["-l", "--locale"], descriptionKey = "sop.locale") + fun setLocale(locale: String) = Locale.setDefault(Locale(locale)) + + @Unmatched + var remainder: MutableList = + mutableListOf() // ignore any other parameters and options in the first parsing phase + } +} 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 47b6123..68b32be 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 @@ -45,7 +45,7 @@ public class SOPTest { @Test @ExpectSystemExitWithStatus(1) public void assertThrowsIfNoSOPBackendSet() { - SopCLI.SOP_INSTANCE = null; + SopCLI.setSopInstance(null); // At this point, no SOP backend is set, so an InvalidStateException triggers exit(1) SopCLI.main(new String[] {"armor"}); } From edef89907410621cbfa89553d3c3d93167096dc6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 12:42:41 +0100 Subject: [PATCH 131/298] Fix GenerateKey --with-key-password option name --- .../src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt index fb5e321..7fa5a70 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt @@ -20,7 +20,7 @@ class GenerateKeyCmd : AbstractSopCmd() { @Parameters(paramLabel = "USERID") var userId: List = listOf() - @Option(names = ["---with-key-password"], paramLabel = "PASSWORD") + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") var withKeyPassword: String? = null @Option(names = ["--profile"], paramLabel = "PROFILE") var profile: String? = null From 41acdfe03a9ee0b8e6732402ae58145220764218 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 12:42:56 +0100 Subject: [PATCH 132/298] ProxyOutputStream: Extend OutputStream --- .../src/main/kotlin/sop/util/ProxyOutputStream.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt index 142f7b3..da6c4fa 100644 --- a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt +++ b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt @@ -15,7 +15,7 @@ import java.io.OutputStream * class is useful if we need to provide an [OutputStream] at one point in time when the final * target output stream is not yet known. */ -class ProxyOutputStream { +class ProxyOutputStream : OutputStream() { private val buffer = ByteArrayOutputStream() private var swapped: OutputStream? = null @@ -27,7 +27,7 @@ class ProxyOutputStream { @Synchronized @Throws(IOException::class) - fun write(b: ByteArray) { + override fun write(b: ByteArray) { if (swapped == null) { buffer.write(b) } else { @@ -37,7 +37,7 @@ class ProxyOutputStream { @Synchronized @Throws(IOException::class) - fun write(b: ByteArray, off: Int, len: Int) { + override fun write(b: ByteArray, off: Int, len: Int) { if (swapped == null) { buffer.write(b, off, len) } else { @@ -47,7 +47,7 @@ class ProxyOutputStream { @Synchronized @Throws(IOException::class) - fun flush() { + override fun flush() { buffer.flush() if (swapped != null) { swapped!!.flush() @@ -56,7 +56,7 @@ class ProxyOutputStream { @Synchronized @Throws(IOException::class) - fun close() { + override fun close() { buffer.close() if (swapped != null) { swapped!!.close() @@ -65,7 +65,7 @@ class ProxyOutputStream { @Synchronized @Throws(IOException::class) - fun write(i: Int) { + override fun write(i: Int) { if (swapped == null) { buffer.write(i) } else { From 0563105b1f071389c431cb6e05df1283366163c3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 13:38:02 +0100 Subject: [PATCH 133/298] Bump version to 8.0.0-SNAPSHOT --- README.md | 2 +- version.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6dd9a81..fa7e5a5 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: 7](https://img.shields.io/badge/Spec%20Revision-7-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/07/) +[![Spec Revision: 8](https://img.shields.io/badge/Spec%20Revision-8-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/08/) [![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) diff --git a/version.gradle b/version.gradle index d8ddfd6..c5a9cb1 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '7.0.1' + shortVersion = '8.0.0' isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 From 2051c3632a3fd8a5fbd2438a8a5a2b91c2819878 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 13:52:36 +0100 Subject: [PATCH 134/298] external-sop: Mark methods with @Nonnull where applicable --- .../main/java/sop/external/ExternalSOP.java | 19 +++++++++++-- .../sop/external/operation/ArmorExternal.java | 8 ++++-- .../operation/ChangeKeyPasswordExternal.java | 11 +++++--- .../external/operation/DearmorExternal.java | 4 ++- .../external/operation/DecryptExternal.java | 27 ++++++++++++------- .../operation/DetachedSignExternal.java | 16 +++++++---- .../operation/DetachedVerifyExternal.java | 16 +++++++---- .../external/operation/EncryptExternal.java | 25 +++++++++++------ .../operation/ExtractCertExternal.java | 5 +++- .../operation/GenerateKeyExternal.java | 13 ++++++--- .../operation/InlineDetachExternal.java | 9 ++++--- .../operation/InlineSignExternal.java | 14 +++++++--- .../operation/InlineVerifyExternal.java | 15 +++++++---- .../operation/ListProfilesExternal.java | 4 ++- .../external/operation/RevokeKeyExternal.java | 8 ++++-- .../external/operation/VersionExternal.java | 6 +++++ 16 files changed, 146 insertions(+), 54 deletions(-) diff --git a/external-sop/src/main/java/sop/external/ExternalSOP.java b/external-sop/src/main/java/sop/external/ExternalSOP.java index e1479ee..3819848 100644 --- a/external-sop/src/main/java/sop/external/ExternalSOP.java +++ b/external-sop/src/main/java/sop/external/ExternalSOP.java @@ -106,76 +106,91 @@ public class ExternalSOP implements SOP { } @Override + @Nonnull public Version version() { return new VersionExternal(binaryName, properties); } @Override + @Nonnull public GenerateKey generateKey() { return new GenerateKeyExternal(binaryName, properties); } @Override + @Nonnull public ExtractCert extractCert() { return new ExtractCertExternal(binaryName, properties); } @Override + @Nonnull public DetachedSign detachedSign() { return new DetachedSignExternal(binaryName, properties, tempDirProvider); } @Override + @Nonnull public InlineSign inlineSign() { return new InlineSignExternal(binaryName, properties); } @Override + @Nonnull public DetachedVerify detachedVerify() { return new DetachedVerifyExternal(binaryName, properties); } @Override + @Nonnull public InlineVerify inlineVerify() { return new InlineVerifyExternal(binaryName, properties, tempDirProvider); } @Override + @Nonnull public InlineDetach inlineDetach() { return new InlineDetachExternal(binaryName, properties, tempDirProvider); } @Override + @Nonnull public Encrypt encrypt() { return new EncryptExternal(binaryName, properties, tempDirProvider); } @Override + @Nonnull public Decrypt decrypt() { return new DecryptExternal(binaryName, properties, tempDirProvider); } @Override + @Nonnull public Armor armor() { return new ArmorExternal(binaryName, properties); } @Override + @Nonnull public ListProfiles listProfiles() { return new ListProfilesExternal(binaryName, properties); } @Override + @Nonnull public RevokeKey revokeKey() { return new RevokeKeyExternal(binaryName, properties); } @Override + @Nonnull public ChangeKeyPassword changeKeyPassword() { return new ChangeKeyPasswordExternal(binaryName, properties); } @Override + @Nonnull public Dearmor dearmor() { return new DearmorExternal(binaryName, properties); } @@ -349,7 +364,7 @@ public class ExternalSOP implements SOP { return new Ready() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public void writeTo(@Nonnull OutputStream outputStream) throws IOException { byte[] buf = new byte[4096]; int r; while ((r = stdIn.read(buf)) >= 0) { @@ -388,7 +403,7 @@ public class ExternalSOP implements SOP { return new Ready() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public void writeTo(@Nonnull OutputStream outputStream) throws IOException { byte[] buf = new byte[4096]; int r; while ((r = standardIn.read(buf)) > 0) { diff --git a/external-sop/src/main/java/sop/external/operation/ArmorExternal.java b/external-sop/src/main/java/sop/external/operation/ArmorExternal.java index 4df7fca..e1d02e9 100644 --- a/external-sop/src/main/java/sop/external/operation/ArmorExternal.java +++ b/external-sop/src/main/java/sop/external/operation/ArmorExternal.java @@ -10,6 +10,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.Armor; +import javax.annotation.Nonnull; import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -30,13 +31,16 @@ public class ArmorExternal implements Armor { } @Override - public Armor label(ArmorLabel label) throws SOPGPException.UnsupportedOption { + @Deprecated + @Nonnull + public Armor label(@Nonnull ArmorLabel label) throws SOPGPException.UnsupportedOption { commandList.add("--label=" + label); return this; } @Override - public Ready data(InputStream data) throws SOPGPException.BadData { + @Nonnull + public Ready data(@Nonnull InputStream data) throws SOPGPException.BadData { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data); } } diff --git a/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java b/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java index 210152f..d53d6a5 100644 --- a/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java +++ b/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java @@ -9,6 +9,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.ChangeKeyPassword; +import javax.annotation.Nonnull; import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -27,13 +28,15 @@ public class ChangeKeyPasswordExternal implements ChangeKeyPassword { } @Override + @Nonnull public ChangeKeyPassword noArmor() { this.commandList.add("--no-armor"); return this; } @Override - public ChangeKeyPassword oldKeyPassphrase(String oldPassphrase) { + @Nonnull + public ChangeKeyPassword oldKeyPassphrase(@Nonnull String oldPassphrase) { this.commandList.add("--old-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter); this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + oldPassphrase); keyPasswordCounter++; @@ -42,7 +45,8 @@ public class ChangeKeyPasswordExternal implements ChangeKeyPassword { } @Override - public ChangeKeyPassword newKeyPassphrase(String newPassphrase) { + @Nonnull + public ChangeKeyPassword newKeyPassphrase(@Nonnull String newPassphrase) { this.commandList.add("--new-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter); this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + newPassphrase); keyPasswordCounter++; @@ -51,7 +55,8 @@ public class ChangeKeyPasswordExternal implements ChangeKeyPassword { } @Override - public Ready keys(InputStream inputStream) throws SOPGPException.KeyIsProtected, SOPGPException.BadData { + @Nonnull + public Ready keys(@Nonnull InputStream inputStream) throws SOPGPException.KeyIsProtected, SOPGPException.BadData { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, inputStream); } } diff --git a/external-sop/src/main/java/sop/external/operation/DearmorExternal.java b/external-sop/src/main/java/sop/external/operation/DearmorExternal.java index bedf018..cd3da6f 100644 --- a/external-sop/src/main/java/sop/external/operation/DearmorExternal.java +++ b/external-sop/src/main/java/sop/external/operation/DearmorExternal.java @@ -9,6 +9,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.Dearmor; +import javax.annotation.Nonnull; import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -29,7 +30,8 @@ public class DearmorExternal implements Dearmor { } @Override - public Ready data(InputStream data) throws SOPGPException.BadData { + @Nonnull + public Ready data(@Nonnull InputStream data) throws SOPGPException.BadData { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data); } } diff --git a/external-sop/src/main/java/sop/external/operation/DecryptExternal.java b/external-sop/src/main/java/sop/external/operation/DecryptExternal.java index 0b91d5b..a1c4016 100644 --- a/external-sop/src/main/java/sop/external/operation/DecryptExternal.java +++ b/external-sop/src/main/java/sop/external/operation/DecryptExternal.java @@ -13,6 +13,7 @@ import sop.external.ExternalSOP; import sop.operation.Decrypt; import sop.util.UTCUtil; +import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -48,21 +49,24 @@ public class DecryptExternal implements Decrypt { } @Override - public Decrypt verifyNotBefore(Date timestamp) + @Nonnull + public Decrypt verifyNotBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { this.commandList.add("--verify-not-before=" + UTCUtil.formatUTCDate(timestamp)); return this; } @Override - public Decrypt verifyNotAfter(Date timestamp) + @Nonnull + public Decrypt verifyNotAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { this.commandList.add("--verify-not-after=" + UTCUtil.formatUTCDate(timestamp)); return this; } @Override - public Decrypt verifyWithCert(InputStream cert) + @Nonnull + public Decrypt verifyWithCert(@Nonnull InputStream cert) throws SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { String envVar = "VERIFY_WITH_" + verifyWithCounter++; commandList.add("--verify-with=@ENV:" + envVar); @@ -71,7 +75,8 @@ public class DecryptExternal implements Decrypt { } @Override - public Decrypt withSessionKey(SessionKey sessionKey) + @Nonnull + public Decrypt withSessionKey(@Nonnull SessionKey sessionKey) throws SOPGPException.UnsupportedOption { String envVar = "SESSION_KEY_" + withSessionKeyCounter++; commandList.add("--with-session-key=@ENV:" + envVar); @@ -80,7 +85,8 @@ public class DecryptExternal implements Decrypt { } @Override - public Decrypt withPassword(String password) + @Nonnull + public Decrypt withPassword(@Nonnull String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { String envVar = "PASSWORD_" + withPasswordCounter++; commandList.add("--with-password=@ENV:" + envVar); @@ -89,7 +95,8 @@ public class DecryptExternal implements Decrypt { } @Override - public Decrypt withKey(InputStream key) + @Nonnull + public Decrypt withKey(@Nonnull InputStream key) throws SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { String envVar = "KEY_" + keyCounter++; commandList.add("@ENV:" + envVar); @@ -98,7 +105,8 @@ public class DecryptExternal implements Decrypt { } @Override - public Decrypt withKeyPassword(byte[] password) + @Nonnull + public Decrypt withKeyPassword(@Nonnull byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { String envVar = "KEY_PASSWORD_" + withKeyPasswordCounter++; commandList.add("--with-key-password=@ENV:" + envVar); @@ -107,7 +115,8 @@ public class DecryptExternal implements Decrypt { } @Override - public ReadyWithResult ciphertext(InputStream ciphertext) + @Nonnull + public ReadyWithResult ciphertext(@Nonnull InputStream ciphertext) throws SOPGPException.BadData, SOPGPException.MissingArg, SOPGPException.CannotDecrypt, SOPGPException.KeyIsProtected, IOException { File tempDir = tempDirProvider.provideTempDirectory(); @@ -131,7 +140,7 @@ public class DecryptExternal implements Decrypt { return new ReadyWithResult() { @Override - public DecryptionResult writeTo(OutputStream outputStream) throws IOException { + public DecryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException { byte[] buf = new byte[4096]; int r; while ((r = ciphertext.read(buf)) > 0) { diff --git a/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java b/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java index 7c579d4..2ef2714 100644 --- a/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java +++ b/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java @@ -12,6 +12,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.DetachedSign; +import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -43,13 +44,15 @@ public class DetachedSignExternal implements DetachedSign { } @Override + @Nonnull public DetachedSign noArmor() { commandList.add("--no-armor"); return this; } @Override - public DetachedSign key(InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { + @Nonnull + public DetachedSign key(@Nonnull InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { String envVar = "KEY_" + keyCounter++; commandList.add("@ENV:" + envVar); envList.add(envVar + "=" + ExternalSOP.readString(key)); @@ -57,7 +60,8 @@ public class DetachedSignExternal implements DetachedSign { } @Override - public DetachedSign withKeyPassword(byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { + @Nonnull + public DetachedSign withKeyPassword(@Nonnull byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { String envVar = "WITH_KEY_PASSWORD_" + withKeyPasswordCounter++; commandList.add("--with-key-password=@ENV:" + envVar); envList.add(envVar + "=" + new String(password)); @@ -65,13 +69,15 @@ public class DetachedSignExternal implements DetachedSign { } @Override - public DetachedSign mode(SignAs mode) throws SOPGPException.UnsupportedOption { + @Nonnull + public DetachedSign mode(@Nonnull SignAs mode) throws SOPGPException.UnsupportedOption { commandList.add("--as=" + mode); return this; } @Override - public ReadyWithResult data(InputStream data) + @Nonnull + public ReadyWithResult data(@Nonnull InputStream data) throws IOException, SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { File tempDir = tempDirProvider.provideTempDirectory(); @@ -88,7 +94,7 @@ public class DetachedSignExternal implements DetachedSign { return new ReadyWithResult() { @Override - public SigningResult writeTo(OutputStream outputStream) throws IOException { + public SigningResult writeTo(@Nonnull OutputStream outputStream) throws IOException { byte[] buf = new byte[4096]; int r; while ((r = data.read(buf)) > 0) { diff --git a/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java b/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java index 2f19c5b..866150d 100644 --- a/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java +++ b/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java @@ -11,6 +11,7 @@ import sop.operation.DetachedVerify; import sop.operation.VerifySignatures; import sop.util.UTCUtil; +import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -42,31 +43,36 @@ public class DetachedVerifyExternal implements DetachedVerify { } @Override - public DetachedVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { + @Nonnull + public DetachedVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { commandList.add("--not-before=" + UTCUtil.formatUTCDate(timestamp)); return this; } @Override - public DetachedVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { + @Nonnull + public DetachedVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { commandList.add("--not-after=" + UTCUtil.formatUTCDate(timestamp)); return this; } @Override - public DetachedVerify cert(InputStream cert) throws SOPGPException.BadData { + @Nonnull + public DetachedVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData { this.certs.add(cert); return this; } @Override - public VerifySignatures signatures(InputStream signatures) throws SOPGPException.BadData { + @Nonnull + public VerifySignatures signatures(@Nonnull InputStream signatures) throws SOPGPException.BadData { this.signatures = signatures; return this; } @Override - public List data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { + @Nonnull + public List data(@Nonnull InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { commandList.add("@ENV:SIGNATURE"); envList.add("SIGNATURE=" + ExternalSOP.readString(signatures)); diff --git a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java index 3eabfc5..f41a36e 100644 --- a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java +++ b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java @@ -12,6 +12,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.Encrypt; +import javax.annotation.Nonnull; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -42,20 +43,23 @@ public class EncryptExternal implements Encrypt { } @Override + @Nonnull public Encrypt noArmor() { this.commandList.add("--no-armor"); return this; } @Override - public Encrypt mode(EncryptAs mode) + @Nonnull + public Encrypt mode(@Nonnull EncryptAs mode) throws SOPGPException.UnsupportedOption { this.commandList.add("--as=" + mode); return this; } @Override - public Encrypt signWith(InputStream key) + @Nonnull + public Encrypt signWith(@Nonnull InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { String envVar = "SIGN_WITH_" + SIGN_WITH_COUNTER++; @@ -65,7 +69,8 @@ public class EncryptExternal implements Encrypt { } @Override - public Encrypt withKeyPassword(byte[] password) + @Nonnull + public Encrypt withKeyPassword(@Nonnull byte[] password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { String envVar = "KEY_PASSWORD_" + KEY_PASSWORD_COUNTER++; commandList.add("--with-key-password=@ENV:" + envVar); @@ -74,7 +79,8 @@ public class EncryptExternal implements Encrypt { } @Override - public Encrypt withPassword(String password) + @Nonnull + public Encrypt withPassword(@Nonnull String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { String envVar = "PASSWORD_" + PASSWORD_COUNTER++; commandList.add("--with-password=@ENV:" + envVar); @@ -83,7 +89,8 @@ public class EncryptExternal implements Encrypt { } @Override - public Encrypt withCert(InputStream cert) + @Nonnull + public Encrypt withCert(@Nonnull InputStream cert) throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { String envVar = "CERT_" + CERT_COUNTER++; @@ -93,13 +100,15 @@ public class EncryptExternal implements Encrypt { } @Override - public Encrypt profile(String profileName) { + @Nonnull + public Encrypt profile(@Nonnull String profileName) { commandList.add("--profile=" + profileName); return this; } @Override - public ReadyWithResult plaintext(InputStream plaintext) + @Nonnull + public ReadyWithResult plaintext(@Nonnull InputStream plaintext) throws SOPGPException.KeyIsProtected, IOException { File tempDir = tempDirProvider.provideTempDirectory(); @@ -116,7 +125,7 @@ public class EncryptExternal implements Encrypt { return new ReadyWithResult() { @Override - public EncryptionResult writeTo(OutputStream outputStream) throws IOException { + public EncryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException { byte[] buf = new byte[4096]; int r; while ((r = plaintext.read(buf)) > 0) { diff --git a/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java b/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java index 5fdcdc1..538359a 100644 --- a/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java +++ b/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java @@ -9,6 +9,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.ExtractCert; +import javax.annotation.Nonnull; import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -29,13 +30,15 @@ public class ExtractCertExternal implements ExtractCert { } @Override + @Nonnull public ExtractCert noArmor() { this.commandList.add("--no-armor"); return this; } @Override - public Ready key(InputStream keyInputStream) throws SOPGPException.BadData { + @Nonnull + public Ready key(@Nonnull InputStream keyInputStream) throws SOPGPException.BadData { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keyInputStream); } } diff --git a/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java b/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java index c46dfb3..a8244cb 100644 --- a/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java +++ b/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java @@ -9,6 +9,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.GenerateKey; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.List; import java.util.Properties; @@ -30,19 +31,22 @@ public class GenerateKeyExternal implements GenerateKey { } @Override + @Nonnull public GenerateKey noArmor() { this.commandList.add("--no-armor"); return this; } @Override - public GenerateKey userId(String userId) { + @Nonnull + public GenerateKey userId(@Nonnull String userId) { this.commandList.add(userId); return this; } @Override - public GenerateKey withKeyPassword(String password) + @Nonnull + public GenerateKey withKeyPassword(@Nonnull String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { this.commandList.add("--with-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter); this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + password); @@ -52,18 +56,21 @@ public class GenerateKeyExternal implements GenerateKey { } @Override - public GenerateKey profile(String profile) { + @Nonnull + public GenerateKey profile(@Nonnull String profile) { commandList.add("--profile=" + profile); return this; } @Override + @Nonnull public GenerateKey signingOnly() { commandList.add("--signing-only"); return this; } @Override + @Nonnull public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { return ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList); diff --git a/external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java b/external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java index c03fe1b..a798006 100644 --- a/external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java +++ b/external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java @@ -10,6 +10,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.InlineDetach; +import javax.annotation.Nonnull; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; @@ -37,13 +38,15 @@ public class InlineDetachExternal implements InlineDetach { } @Override + @Nonnull public InlineDetach noArmor() { commandList.add("--no-armor"); return this; } @Override - public ReadyWithResult message(InputStream messageInputStream) throws IOException, SOPGPException.BadData { + @Nonnull + public ReadyWithResult message(@Nonnull InputStream messageInputStream) throws IOException, SOPGPException.BadData { File tempDir = tempDirProvider.provideTempDirectory(); File signaturesOut = new File(tempDir, "signatures"); @@ -60,7 +63,7 @@ public class InlineDetachExternal implements InlineDetach { return new ReadyWithResult() { @Override - public Signatures writeTo(OutputStream outputStream) throws IOException { + public Signatures writeTo(@Nonnull OutputStream outputStream) throws IOException { byte[] buf = new byte[4096]; int r; while ((r = messageInputStream.read(buf)) > 0) { @@ -90,7 +93,7 @@ public class InlineDetachExternal implements InlineDetach { final byte[] sigBytes = signaturesBuffer.toByteArray(); return new Signatures() { @Override - public void writeTo(OutputStream signatureOutputStream) throws IOException { + public void writeTo(@Nonnull OutputStream signatureOutputStream) throws IOException { signatureOutputStream.write(sigBytes); } }; diff --git a/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java b/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java index 68a630d..1a86002 100644 --- a/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java +++ b/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java @@ -10,6 +10,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.InlineSign; +import javax.annotation.Nonnull; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -34,13 +35,15 @@ public class InlineSignExternal implements InlineSign { } @Override + @Nonnull public InlineSign noArmor() { commandList.add("--no-armor"); return this; } @Override - public InlineSign key(InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { + @Nonnull + public InlineSign key(@Nonnull InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { String envVar = "KEY_" + keyCounter++; commandList.add("@ENV:" + envVar); envList.add(envVar + "=" + ExternalSOP.readString(key)); @@ -48,7 +51,8 @@ public class InlineSignExternal implements InlineSign { } @Override - public InlineSign withKeyPassword(byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { + @Nonnull + public InlineSign withKeyPassword(@Nonnull byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { String envVar = "WITH_KEY_PASSWORD_" + withKeyPasswordCounter++; commandList.add("--with-key-password=@ENV:" + envVar); envList.add(envVar + "=" + new String(password)); @@ -56,13 +60,15 @@ public class InlineSignExternal implements InlineSign { } @Override - public InlineSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption { + @Nonnull + public InlineSign mode(@Nonnull InlineSignAs mode) throws SOPGPException.UnsupportedOption { commandList.add("--as=" + mode); return this; } @Override - public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { + @Nonnull + public Ready data(@Nonnull InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data); } } diff --git a/external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java b/external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java index 8010367..10a2d47 100644 --- a/external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java +++ b/external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java @@ -11,6 +11,7 @@ import sop.external.ExternalSOP; import sop.operation.InlineVerify; import sop.util.UTCUtil; +import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -42,19 +43,22 @@ public class InlineVerifyExternal implements InlineVerify { } @Override - public InlineVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { + @Nonnull + public InlineVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { commandList.add("--not-before=" + UTCUtil.formatUTCDate(timestamp)); return this; } @Override - public InlineVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { + @Nonnull + public InlineVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { commandList.add("--not-after=" + UTCUtil.formatUTCDate(timestamp)); return this; } @Override - public InlineVerify cert(InputStream cert) throws SOPGPException.BadData, IOException { + @Nonnull + public InlineVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData, IOException { String envVar = "CERT_" + certCounter++; commandList.add("@ENV:" + envVar); envList.add(envVar + "=" + ExternalSOP.readString(cert)); @@ -62,7 +66,8 @@ public class InlineVerifyExternal implements InlineVerify { } @Override - public ReadyWithResult> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { + @Nonnull + public ReadyWithResult> data(@Nonnull InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { File tempDir = tempDirProvider.provideTempDirectory(); File verificationsOut = new File(tempDir, "verifications-out"); @@ -79,7 +84,7 @@ public class InlineVerifyExternal implements InlineVerify { return new ReadyWithResult>() { @Override - public List writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { + public List writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature { byte[] buf = new byte[4096]; int r; while ((r = data.read(buf)) > 0) { diff --git a/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java b/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java index 0c76b63..21d5e13 100644 --- a/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java +++ b/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java @@ -8,6 +8,7 @@ import sop.Profile; import sop.external.ExternalSOP; import sop.operation.ListProfiles; +import javax.annotation.Nonnull; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -25,7 +26,8 @@ public class ListProfilesExternal implements ListProfiles { } @Override - public List subcommand(String command) { + @Nonnull + public List subcommand(@Nonnull String command) { commandList.add(command); try { String output = new String(ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList).getBytes()); diff --git a/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java b/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java index e7aad9d..72ed549 100644 --- a/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java +++ b/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java @@ -9,6 +9,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.RevokeKey; +import javax.annotation.Nonnull; import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -28,13 +29,15 @@ public class RevokeKeyExternal implements RevokeKey { } @Override + @Nonnull public RevokeKey noArmor() { this.commandList.add("--no-armor"); return this; } @Override - public RevokeKey withKeyPassword(byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { + @Nonnull + public RevokeKey withKeyPassword(@Nonnull byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { String envVar = "KEY_PASSWORD_" + withKeyPasswordCounter++; commandList.add("--with-key-password=@ENV:" + envVar); envList.add(envVar + "=" + new String(password)); @@ -42,7 +45,8 @@ public class RevokeKeyExternal implements RevokeKey { } @Override - public Ready keys(InputStream keys) { + @Nonnull + public Ready keys(@Nonnull InputStream keys) { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys); } } diff --git a/external-sop/src/main/java/sop/external/operation/VersionExternal.java b/external-sop/src/main/java/sop/external/operation/VersionExternal.java index 0b9c5b4..ab2bbef 100644 --- a/external-sop/src/main/java/sop/external/operation/VersionExternal.java +++ b/external-sop/src/main/java/sop/external/operation/VersionExternal.java @@ -7,6 +7,7 @@ package sop.external.operation; import sop.external.ExternalSOP; import sop.operation.Version; +import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -27,6 +28,7 @@ public class VersionExternal implements Version { } @Override + @Nonnull public String getName() { String[] command = new String[] {binary, "version"}; String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); @@ -45,6 +47,7 @@ public class VersionExternal implements Version { } @Override + @Nonnull public String getVersion() { String[] command = new String[] {binary, "version"}; String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); @@ -63,6 +66,7 @@ public class VersionExternal implements Version { } @Override + @Nonnull public String getBackendVersion() { String[] command = new String[] {binary, "version", "--backend"}; String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); @@ -82,6 +86,7 @@ public class VersionExternal implements Version { } @Override + @Nonnull public String getExtendedVersion() { String[] command = new String[] {binary, "version", "--extended"}; String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); @@ -137,6 +142,7 @@ public class VersionExternal implements Version { } @Override + @Nonnull public String getSopSpecVersion() { String[] command = new String[] {binary, "version", "--sop-spec"}; String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); From 3dde1748805f4c665fc29fb6ab816a8f490eb464 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 14:26:24 +0100 Subject: [PATCH 135/298] Fix woodpecker pipeline --- .woodpecker/.build.yml | 6 +++--- .woodpecker/.reuse.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.woodpecker/.build.yml b/.woodpecker/.build.yml index d72d19e..ff59c4e 100644 --- a/.woodpecker/.build.yml +++ b/.woodpecker/.build.yml @@ -1,6 +1,6 @@ -pipeline: +steps: run: - image: gradle:7.5-jdk8-jammy + image: gradle:7.6-jdk11-jammy commands: # Install Sequoia-SOP - apt update && apt install --yes sqop @@ -14,4 +14,4 @@ pipeline: - gradle check javadocAll # Code has coverage - gradle jacocoRootReport coveralls - secrets: [COVERALLS_REPO_TOKEN] + secrets: [coveralls_repo_token] diff --git a/.woodpecker/.reuse.yml b/.woodpecker/.reuse.yml index 58f17e6..d78c61e 100644 --- a/.woodpecker/.reuse.yml +++ b/.woodpecker/.reuse.yml @@ -1,6 +1,6 @@ # Code is licensed properly # See https://reuse.software/ -pipeline: +steps: reuse: image: fsfe/reuse:latest commands: From 03cabdf3fb0d0ac25c849480a47d5e255d219644 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 15:22:40 +0100 Subject: [PATCH 136/298] Add tests for --no-armor for change-key-password and revoke-key --- .../operation/ChangeKeyPasswordTest.java | 26 +++++++++++++++++++ .../testsuite/operation/RevokeKeyTest.java | 9 +++++++ 2 files changed, 35 insertions(+) diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java index b575047..8948dda 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java @@ -10,6 +10,8 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import sop.SOP; import sop.exception.SOPGPException; +import sop.testsuite.JUtils; +import sop.testsuite.TestData; import sop.util.UTF8Util; import java.io.IOException; @@ -17,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") @@ -95,4 +98,27 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { sop.changeKeyPassword().newKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testNoArmor(SOP sop) throws IOException { + byte[] oldPassword = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + byte[] newPassword = "0r4ng3".getBytes(UTF8Util.UTF8); + byte[] protectedKey = sop.generateKey().withKeyPassword(oldPassword).generate().getBytes(); + + byte[] armored = sop.changeKeyPassword() + .oldKeyPassphrase(oldPassword) + .newKeyPassphrase(newPassword) + .keys(protectedKey) + .getBytes(); + JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); + + byte[] unarmored = sop.changeKeyPassword() + .noArmor() + .oldKeyPassphrase(oldPassword) + .newKeyPassphrase(newPassword) + .keys(protectedKey) + .getBytes(); + assertFalse(JUtils.arrayStartsWith(unarmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK)); + } } diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java index 6595133..cb51332 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java @@ -43,6 +43,15 @@ public class RevokeKeyTest extends AbstractSOPTest { assertFalse(Arrays.equals(secretKey, revocation)); } + @ParameterizedTest + @MethodSource("provideInstances") + public void revokeUnprotectedKeyNoArmor(SOP sop) throws IOException { + byte[] secretKey = sop.generateKey().userId("Alice ").generate().getBytes(); + byte[] revocation = sop.revokeKey().noArmor().keys(secretKey).getBytes(); + + assertFalse(JUtils.arrayStartsWith(revocation, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); + } + @ParameterizedTest @MethodSource("provideInstances") public void revokeUnprotectedKeyUnarmored(SOP sop) throws IOException { From 802bc0aa73a4357912646b8e6cd9e2b0134cd021 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 15:24:09 +0100 Subject: [PATCH 137/298] ArmorCMD: Drop --label option --- .../kotlin/sop/cli/picocli/commands/ArmorCmd.kt | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt index ccfba4d..50716f1 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt @@ -6,9 +6,7 @@ package sop.cli.picocli.commands import java.io.IOException import picocli.CommandLine.Command -import picocli.CommandLine.Option import sop.cli.picocli.SopCLI -import sop.enums.ArmorLabel import sop.exception.SOPGPException.BadData import sop.exception.SOPGPException.UnsupportedOption @@ -18,21 +16,9 @@ import sop.exception.SOPGPException.UnsupportedOption exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) class ArmorCmd : AbstractSopCmd() { - @Option(names = ["--label"], paramLabel = "{auto|sig|key|cert|message}") - var label: ArmorLabel? = null - override fun run() { val armor = throwIfUnsupportedSubcommand(SopCLI.getSop().armor(), "armor") - label?.let { - try { - armor.label(it) - } catch (unsupported: UnsupportedOption) { - val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--label") - throw UnsupportedOption(errorMsg, unsupported) - } - } - try { val ready = armor.data(System.`in`) ready.writeTo(System.out) From d24ff9cbde0520437f308ed61369294f81484a7c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 15:26:06 +0100 Subject: [PATCH 138/298] Remove label related CLI tests --- .../cli/picocli/commands/ArmorCmdTest.java | 53 +++++-------------- 1 file changed, 12 insertions(+), 41 deletions(-) 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 6bdbe7f..da211e0 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,17 +4,6 @@ package sop.cli.picocli.commands; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -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 java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import com.ginsberg.junit.exit.FailOnSystemExit; import org.junit.jupiter.api.BeforeEach; @@ -22,10 +11,20 @@ import org.junit.jupiter.api.Test; import sop.Ready; import sop.SOP; import sop.cli.picocli.SopCLI; -import sop.enums.ArmorLabel; import sop.exception.SOPGPException; import sop.operation.Armor; +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + public class ArmorCmdTest { private Armor armor; @@ -41,40 +40,12 @@ public class ArmorCmdTest { SopCLI.setSopInstance(sop); } - @Test - public void assertLabelIsNotCalledByDefault() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"armor"}); - verify(armor, never()).label(any()); - } - - @Test - public void assertLabelIsCalledWhenFlaggedWithArgument() throws SOPGPException.UnsupportedOption { - for (ArmorLabel label : ArmorLabel.values()) { - SopCLI.main(new String[] {"armor", "--label", label.name()}); - verify(armor, times(1)).label(label); - } - } - @Test public void assertDataIsAlwaysCalled() throws SOPGPException.BadData, IOException { SopCLI.main(new String[] {"armor"}); verify(armor, times(1)).data((InputStream) any()); } - @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void assertThrowsForInvalidLabel() { - SopCLI.main(new String[] {"armor", "--label", "Invalid"}); - } - - @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void ifLabelsUnsupportedExit37() throws SOPGPException.UnsupportedOption { - when(armor.label(any())).thenThrow(new SOPGPException.UnsupportedOption("Custom Armor labels are not supported.")); - - SopCLI.main(new String[] {"armor", "--label", "Sig"}); - } - @Test @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void ifBadDataExit41() throws SOPGPException.BadData, IOException { @@ -94,7 +65,7 @@ public class ArmorCmdTest { private static Ready nopReady() { return new Ready() { @Override - public void writeTo(OutputStream outputStream) { + public void writeTo(@Nonnull OutputStream outputStream) { } }; } From 1c0666b4e13a0199c5889fd422905a4af9059134 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 16:27:19 +0100 Subject: [PATCH 139/298] Kotlin conversion: ExternalSOP --- .../main/java/sop/external/ExternalSOP.java | 469 ------------------ .../main/kotlin/sop/external/ExternalSOP.kt | 318 ++++++++++++ 2 files changed, 318 insertions(+), 469 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/ExternalSOP.java create mode 100644 external-sop/src/main/kotlin/sop/external/ExternalSOP.kt diff --git a/external-sop/src/main/java/sop/external/ExternalSOP.java b/external-sop/src/main/java/sop/external/ExternalSOP.java deleted file mode 100644 index 3819848..0000000 --- a/external-sop/src/main/java/sop/external/ExternalSOP.java +++ /dev/null @@ -1,469 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external; - -import sop.Ready; -import sop.SOP; -import sop.exception.SOPGPException; -import sop.external.operation.ArmorExternal; -import sop.external.operation.ChangeKeyPasswordExternal; -import sop.external.operation.DearmorExternal; -import sop.external.operation.DecryptExternal; -import sop.external.operation.DetachedSignExternal; -import sop.external.operation.DetachedVerifyExternal; -import sop.external.operation.EncryptExternal; -import sop.external.operation.ExtractCertExternal; -import sop.external.operation.GenerateKeyExternal; -import sop.external.operation.InlineDetachExternal; -import sop.external.operation.InlineSignExternal; -import sop.external.operation.InlineVerifyExternal; -import sop.external.operation.ListProfilesExternal; -import sop.external.operation.RevokeKeyExternal; -import sop.external.operation.VersionExternal; -import sop.operation.Armor; -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; - -import javax.annotation.Nonnull; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.attribute.FileAttribute; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link SOP} API using an external SOP binary. - */ -public class ExternalSOP implements SOP { - - private final String binaryName; - private final Properties properties; - private final TempDirProvider tempDirProvider; - - /** - * Instantiate an {@link ExternalSOP} object for the given binary and pass it empty environment variables, - * as well as a default {@link TempDirProvider}. - * - * @param binaryName name / path of the SOP binary - */ - public ExternalSOP(@Nonnull String binaryName) { - this(binaryName, new Properties()); - } - - /** - * Instantiate an {@link ExternalSOP} object for the given binary, and pass it the given properties as - * environment variables, as well as a default {@link TempDirProvider}. - * - * @param binaryName name / path of the SOP binary - * @param properties environment variables - */ - public ExternalSOP(@Nonnull String binaryName, @Nonnull Properties properties) { - this(binaryName, properties, defaultTempDirProvider()); - } - - /** - * Instantiate an {@link ExternalSOP} object for the given binary and the given {@link TempDirProvider} - * using empty environment variables. - * - * @param binaryName name / path of the SOP binary - * @param tempDirProvider custom tempDirProvider - */ - public ExternalSOP(@Nonnull String binaryName, @Nonnull TempDirProvider tempDirProvider) { - this(binaryName, new Properties(), tempDirProvider); - } - - /** - * Instantiate an {@link ExternalSOP} object for the given binary using the given properties and - * custom {@link TempDirProvider}. - * - * @param binaryName name / path of the SOP binary - * @param properties environment variables - * @param tempDirProvider tempDirProvider - */ - public ExternalSOP(@Nonnull String binaryName, @Nonnull Properties properties, @Nonnull TempDirProvider tempDirProvider) { - this.binaryName = binaryName; - this.properties = properties; - this.tempDirProvider = tempDirProvider; - } - - @Override - @Nonnull - public Version version() { - return new VersionExternal(binaryName, properties); - } - - @Override - @Nonnull - public GenerateKey generateKey() { - return new GenerateKeyExternal(binaryName, properties); - } - - @Override - @Nonnull - public ExtractCert extractCert() { - return new ExtractCertExternal(binaryName, properties); - } - - @Override - @Nonnull - public DetachedSign detachedSign() { - return new DetachedSignExternal(binaryName, properties, tempDirProvider); - } - - @Override - @Nonnull - public InlineSign inlineSign() { - return new InlineSignExternal(binaryName, properties); - } - - @Override - @Nonnull - public DetachedVerify detachedVerify() { - return new DetachedVerifyExternal(binaryName, properties); - } - - @Override - @Nonnull - public InlineVerify inlineVerify() { - return new InlineVerifyExternal(binaryName, properties, tempDirProvider); - } - - @Override - @Nonnull - public InlineDetach inlineDetach() { - return new InlineDetachExternal(binaryName, properties, tempDirProvider); - } - - @Override - @Nonnull - public Encrypt encrypt() { - return new EncryptExternal(binaryName, properties, tempDirProvider); - } - - @Override - @Nonnull - public Decrypt decrypt() { - return new DecryptExternal(binaryName, properties, tempDirProvider); - } - - @Override - @Nonnull - public Armor armor() { - return new ArmorExternal(binaryName, properties); - } - - @Override - @Nonnull - public ListProfiles listProfiles() { - return new ListProfilesExternal(binaryName, properties); - } - - @Override - @Nonnull - public RevokeKey revokeKey() { - return new RevokeKeyExternal(binaryName, properties); - } - - @Override - @Nonnull - public ChangeKeyPassword changeKeyPassword() { - return new ChangeKeyPasswordExternal(binaryName, properties); - } - - @Override - @Nonnull - public Dearmor dearmor() { - return new DearmorExternal(binaryName, properties); - } - - public static void finish(@Nonnull Process process) throws IOException { - try { - mapExitCodeOrException(process); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - /** - * Wait for the {@link Process} to finish and read out its exit code. - * If the exit code is {@value "0"}, this method just returns. - * Otherwise, the exit code gets mapped to a {@link SOPGPException} which then gets thrown. - * If the exit code does not match any of the known exit codes defined in the SOP specification, - * this method throws a {@link RuntimeException} instead. - * - * @param process process - * @throws InterruptedException if the thread is interrupted before the process could exit - * @throws IOException in case of an IO error - */ - private static void mapExitCodeOrException(@Nonnull Process process) throws InterruptedException, IOException { - // wait for process termination - int exitCode = process.waitFor(); - - if (exitCode == 0) { - // we're good, bye - return; - } - - // Read error message - InputStream errIn = process.getErrorStream(); - String errorMessage = readString(errIn); - - switch (exitCode) { - case SOPGPException.NoSignature.EXIT_CODE: - throw new SOPGPException.NoSignature("External SOP backend reported error NoSignature (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE: - throw new UnsupportedOperationException("External SOP backend reported error UnsupportedAsymmetricAlgo (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.CertCannotEncrypt.EXIT_CODE: - throw new SOPGPException.CertCannotEncrypt("External SOP backend reported error CertCannotEncrypt (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.MissingArg.EXIT_CODE: - throw new SOPGPException.MissingArg("External SOP backend reported error MissingArg (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.IncompleteVerification.EXIT_CODE: - throw new SOPGPException.IncompleteVerification("External SOP backend reported error IncompleteVerification (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.CannotDecrypt.EXIT_CODE: - throw new SOPGPException.CannotDecrypt("External SOP backend reported error CannotDecrypt (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.PasswordNotHumanReadable.EXIT_CODE: - throw new SOPGPException.PasswordNotHumanReadable("External SOP backend reported error PasswordNotHumanReadable (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.UnsupportedOption.EXIT_CODE: - throw new SOPGPException.UnsupportedOption("External SOP backend reported error UnsupportedOption (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.BadData.EXIT_CODE: - throw new SOPGPException.BadData("External SOP backend reported error BadData (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.ExpectedText.EXIT_CODE: - throw new SOPGPException.ExpectedText("External SOP backend reported error ExpectedText (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.OutputExists.EXIT_CODE: - throw new SOPGPException.OutputExists("External SOP backend reported error OutputExists (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.MissingInput.EXIT_CODE: - throw new SOPGPException.MissingInput("External SOP backend reported error MissingInput (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.KeyIsProtected.EXIT_CODE: - throw new SOPGPException.KeyIsProtected("External SOP backend reported error KeyIsProtected (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.UnsupportedSubcommand.EXIT_CODE: - throw new SOPGPException.UnsupportedSubcommand("External SOP backend reported error UnsupportedSubcommand (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.UnsupportedSpecialPrefix.EXIT_CODE: - throw new SOPGPException.UnsupportedSpecialPrefix("External SOP backend reported error UnsupportedSpecialPrefix (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.AmbiguousInput.EXIT_CODE: - throw new SOPGPException.AmbiguousInput("External SOP backend reported error AmbiguousInput (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.KeyCannotSign.EXIT_CODE: - throw new SOPGPException.KeyCannotSign("External SOP backend reported error KeyCannotSign (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.IncompatibleOptions.EXIT_CODE: - throw new SOPGPException.IncompatibleOptions("External SOP backend reported error IncompatibleOptions (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.UnsupportedProfile.EXIT_CODE: - throw new SOPGPException.UnsupportedProfile("External SOP backend reported error UnsupportedProfile (" + - exitCode + "):\n" + errorMessage); - - default: - // Did you forget to add a case for a new exception type? - throw new RuntimeException("External SOP backend reported unknown exit code (" + - exitCode + "):\n" + errorMessage); - } - } - - /** - * Return all key-value pairs from the given {@link Properties} object as a list with items of the form - *
key=value
. - * - * @param properties properties - * @return list of key=value strings - */ - public static List propertiesToEnv(@Nonnull Properties properties) { - List env = new ArrayList<>(); - for (Object key : properties.keySet()) { - env.add(key + "=" + properties.get(key)); - } - return env; - } - - /** - * Read the contents of the {@link InputStream} and return them as a {@link String}. - * - * @param inputStream input stream - * @return string - * @throws IOException in case of an IO error - */ - public static String readString(@Nonnull InputStream inputStream) throws IOException { - ByteArrayOutputStream bOut = new ByteArrayOutputStream(); - byte[] buf = new byte[4096]; - int r; - while ((r = inputStream.read(buf)) > 0) { - bOut.write(buf, 0, r); - } - return bOut.toString(); - } - - /** - * Execute the given command on the given {@link Runtime} with the given list of environment variables. - * This command does not transform any input data, and instead is purely a producer. - * - * @param runtime runtime - * @param commandList command - * @param envList environment variables - * @return ready to read the result from - */ - public static Ready executeProducingOperation(@Nonnull Runtime runtime, - @Nonnull List commandList, - @Nonnull List envList) { - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - - try { - Process process = runtime.exec(command, env); - InputStream stdIn = process.getInputStream(); - - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - byte[] buf = new byte[4096]; - int r; - while ((r = stdIn.read(buf)) >= 0) { - outputStream.write(buf, 0, r); - } - - outputStream.flush(); - outputStream.close(); - - ExternalSOP.finish(process); - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Execute the given command on the given runtime using the given environment variables. - * The given input stream provides input for the process. - * This command is a transformation, meaning it is given input data and transforms it into output data. - * - * @param runtime runtime - * @param commandList command - * @param envList environment variables - * @param standardIn stream of input data for the process - * @return ready to read the result from - */ - public static Ready executeTransformingOperation(@Nonnull Runtime runtime, @Nonnull List commandList, @Nonnull List envList, @Nonnull InputStream standardIn) { - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - try { - Process process = runtime.exec(command, env); - OutputStream processOut = process.getOutputStream(); - InputStream processIn = process.getInputStream(); - - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - byte[] buf = new byte[4096]; - int r; - while ((r = standardIn.read(buf)) > 0) { - processOut.write(buf, 0, r); - } - standardIn.close(); - - try { - processOut.flush(); - processOut.close(); - } catch (IOException e) { - // Perhaps the stream is already closed, in which case we ignore the exception. - if (!"Stream closed".equals(e.getMessage())) { - throw e; - } - } - - while ((r = processIn.read(buf)) > 0) { - outputStream.write(buf, 0 , r); - } - processIn.close(); - - outputStream.flush(); - outputStream.close(); - - finish(process); - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * 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 parse them out from there. - * Unfortunately, on Java you cannot open {@link java.io.FileDescriptor FileDescriptors} arbitrarily, so we - * have to rely on temporary files to pass results. - * An example: - *
sop decrypt
can emit signature verifications via
--verify-out=/path/to/tempfile
. - * {@link DecryptExternal} will then parse the temp file to make the result available to consumers. - * Temporary files are deleted after being read, yet creating temp files for sensitive information on disk - * might pose a security risk. Use with care! - */ - public interface TempDirProvider { - File provideTempDirectory() throws IOException; - } - - /** - * Default implementation of the {@link TempDirProvider} which stores temporary files in the systems temp dir - * ({@link Files#createTempDirectory(String, FileAttribute[])}). - * - * @return default implementation - */ - public static TempDirProvider defaultTempDirProvider() { - return new TempDirProvider() { - @Override - public File provideTempDirectory() throws IOException { - return Files.createTempDirectory("ext-sop").toFile(); - } - }; - } -} diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt new file mode 100644 index 0000000..3a0ef52 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -0,0 +1,318 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external + +import java.io.* +import java.nio.file.Files +import java.util.* +import javax.annotation.Nonnull +import sop.Ready +import sop.SOP +import sop.exception.SOPGPException.* +import sop.external.ExternalSOP.TempDirProvider +import sop.external.operation.* +import sop.operation.* + +/** + * Implementation of the {@link SOP} API using an external SOP binary. + * + * Instantiate an [ExternalSOP] object for the given binary and the given [TempDirProvider] using + * empty environment variables. + * + * @param binaryName name / path of the SOP binary + * @param tempDirProvider custom tempDirProvider + */ +class ExternalSOP( + private val binaryName: String, + private val properties: Properties = Properties(), + private val tempDirProvider: TempDirProvider = defaultTempDirProvider() +) : SOP { + + constructor( + binaryName: String, + properties: Properties + ) : this(binaryName, properties, defaultTempDirProvider()) + + override fun version(): Version = VersionExternal(binaryName, properties) + + override fun generateKey(): GenerateKey = GenerateKeyExternal(binaryName, properties) + + override fun extractCert(): ExtractCert = ExtractCertExternal(binaryName, properties) + + override fun detachedSign(): DetachedSign = + DetachedSignExternal(binaryName, properties, tempDirProvider) + + override fun inlineSign(): InlineSign = InlineSignExternal(binaryName, properties) + + override fun detachedVerify(): DetachedVerify = DetachedVerifyExternal(binaryName, properties) + + override fun inlineVerify(): InlineVerify = + InlineVerifyExternal(binaryName, properties, tempDirProvider) + + override fun inlineDetach(): InlineDetach = + InlineDetachExternal(binaryName, properties, tempDirProvider) + + override fun encrypt(): Encrypt = EncryptExternal(binaryName, properties, tempDirProvider) + + override fun decrypt(): Decrypt = DecryptExternal(binaryName, properties, tempDirProvider) + + override fun armor(): Armor = ArmorExternal(binaryName, properties) + + override fun dearmor(): Dearmor = DearmorExternal(binaryName, properties) + + override fun listProfiles(): ListProfiles = ListProfilesExternal(binaryName, properties) + + override fun revokeKey(): RevokeKey = RevokeKeyExternal(binaryName, properties) + + override fun changeKeyPassword(): ChangeKeyPassword = + ChangeKeyPasswordExternal(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 + * parse them out from there. Unfortunately, on Java you cannot open + * [FileDescriptors][java.io.FileDescriptor] arbitrarily, so we have to rely on temporary files + * to pass results. An example: `sop decrypt` can emit signature verifications via + * `--verify-out=/path/to/tempfile`. [DecryptExternal] will then parse the temp file to make the + * result available to consumers. Temporary files are deleted after being read, yet creating + * temp files for sensitive information on disk might pose a security risk. Use with care! + */ + fun interface TempDirProvider { + + @Throws(IOException::class) fun provideTempDirectory(): File + } + + companion object { + + @JvmStatic + @Throws(IOException::class) + fun finish(process: Process) { + try { + mapExitCodeOrException(process) + } catch (e: InterruptedException) { + throw RuntimeException(e) + } + } + + @JvmStatic + @Throws(InterruptedException::class, IOException::class) + private fun mapExitCodeOrException(process: Process) { + // wait for process termination + val exitCode = process.waitFor() + + if (exitCode == 0) { + // we're good, bye + return + } + + // Read error message + val errIn = process.errorStream + val errorMessage = readString(errIn) + + when (exitCode) { + NoSignature.EXIT_CODE -> + throw NoSignature( + "External SOP backend reported error NoSignature ($exitCode):\n$errorMessage") + UnsupportedAsymmetricAlgo.EXIT_CODE -> + throw UnsupportedOperationException( + "External SOP backend reported error UnsupportedAsymmetricAlgo ($exitCode):\n$errorMessage") + CertCannotEncrypt.EXIT_CODE -> + throw CertCannotEncrypt( + "External SOP backend reported error CertCannotEncrypt ($exitCode):\n$errorMessage") + MissingArg.EXIT_CODE -> + throw MissingArg( + "External SOP backend reported error MissingArg ($exitCode):\n$errorMessage") + IncompleteVerification.EXIT_CODE -> + throw IncompleteVerification( + "External SOP backend reported error IncompleteVerification ($exitCode):\n$errorMessage") + CannotDecrypt.EXIT_CODE -> + throw CannotDecrypt( + "External SOP backend reported error CannotDecrypt ($exitCode):\n$errorMessage") + PasswordNotHumanReadable.EXIT_CODE -> + throw PasswordNotHumanReadable( + "External SOP backend reported error PasswordNotHumanReadable ($exitCode):\n$errorMessage") + UnsupportedOption.EXIT_CODE -> + throw UnsupportedOption( + "External SOP backend reported error UnsupportedOption ($exitCode):\n$errorMessage") + BadData.EXIT_CODE -> + throw BadData( + "External SOP backend reported error BadData ($exitCode):\n$errorMessage") + ExpectedText.EXIT_CODE -> + throw ExpectedText( + "External SOP backend reported error ExpectedText ($exitCode):\n$errorMessage") + OutputExists.EXIT_CODE -> + throw OutputExists( + "External SOP backend reported error OutputExists ($exitCode):\n$errorMessage") + MissingInput.EXIT_CODE -> + throw MissingInput( + "External SOP backend reported error MissingInput ($exitCode):\n$errorMessage") + KeyIsProtected.EXIT_CODE -> + throw KeyIsProtected( + "External SOP backend reported error KeyIsProtected ($exitCode):\n$errorMessage") + UnsupportedSubcommand.EXIT_CODE -> + throw UnsupportedSubcommand( + "External SOP backend reported error UnsupportedSubcommand ($exitCode):\n$errorMessage") + UnsupportedSpecialPrefix.EXIT_CODE -> + throw UnsupportedSpecialPrefix( + "External SOP backend reported error UnsupportedSpecialPrefix ($exitCode):\n$errorMessage") + AmbiguousInput.EXIT_CODE -> + throw AmbiguousInput( + "External SOP backend reported error AmbiguousInput ($exitCode):\n$errorMessage") + KeyCannotSign.EXIT_CODE -> + throw KeyCannotSign( + "External SOP backend reported error KeyCannotSign ($exitCode):\n$errorMessage") + IncompatibleOptions.EXIT_CODE -> + throw IncompatibleOptions( + "External SOP backend reported error IncompatibleOptions ($exitCode):\n$errorMessage") + UnsupportedProfile.EXIT_CODE -> + throw UnsupportedProfile( + "External SOP backend reported error UnsupportedProfile ($exitCode):\n$errorMessage") + + // Did you forget to add a case for a new exception type? + else -> + throw RuntimeException( + "External SOP backend reported unknown exit code ($exitCode):\n$errorMessage") + } + } + + /** + * Return all key-value pairs from the given [Properties] object as a list with items of the + * form `key=value`. + * + * @param properties properties + * @return list of key=value strings + */ + @JvmStatic + fun propertiesToEnv(properties: Properties): List = + properties.map { "${it.key}=${it.value}" } + + /** + * Read the contents of the [InputStream] and return them as a [String]. + * + * @param inputStream input stream + * @return string + * @throws IOException in case of an IO error + */ + @JvmStatic + @Throws(IOException::class) + fun readString(inputStream: InputStream): String { + val bOut = ByteArrayOutputStream() + val buf = ByteArray(4096) + var r: Int + while (inputStream.read(buf).also { r = it } > 0) { + bOut.write(buf, 0, r) + } + return bOut.toString() + } + + /** + * Execute the given command on the given [Runtime] with the given list of environment + * variables. This command does not transform any input data, and instead is purely a + * producer. + * + * @param runtime runtime + * @param commandList command + * @param envList environment variables + * @return ready to read the result from + */ + @JvmStatic + fun executeProducingOperation( + runtime: Runtime, + commandList: List, + envList: List + ): Ready { + try { + val process = runtime.exec(commandList.toTypedArray(), envList.toTypedArray()) + val stdIn = process.inputStream + + return object : Ready() { + @Throws(IOException::class) + override fun writeTo(@Nonnull outputStream: OutputStream) { + val buf = ByteArray(4096) + var r: Int + while (stdIn.read(buf).also { r = it } >= 0) { + outputStream.write(buf, 0, r) + } + outputStream.flush() + outputStream.close() + finish(process) + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + /** + * Execute the given command on the given runtime using the given environment variables. The + * given input stream provides input for the process. This command is a transformation, + * meaning it is given input data and transforms it into output data. + * + * @param runtime runtime + * @param commandList command + * @param envList environment variables + * @param standardIn stream of input data for the process + * @return ready to read the result from + */ + @JvmStatic + fun executeTransformingOperation( + runtime: Runtime, + commandList: List, + envList: List, + standardIn: InputStream + ): Ready { + try { + val process = runtime.exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + val buf = ByteArray(4096) + var r: Int + while (standardIn.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + standardIn.close() + + try { + processOut.flush() + processOut.close() + } catch (e: IOException) { + // Perhaps the stream is already closed, in which case we ignore the + // exception. + if ("Stream closed" != e.message) { + throw e + } + } + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + processIn.close() + + outputStream.flush() + outputStream.close() + + finish(process) + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + /** + * 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-sop").toFile() } + } + } +} From 67719526188a5ccbf1b4087fd4924c899b24703f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 16:33:44 +0100 Subject: [PATCH 140/298] Kotlin conversion: ArmorExternal --- .../sop/external/operation/ArmorExternal.java | 46 ------------------- .../sop/external/operation/ArmorExternal.kt | 26 +++++++++++ 2 files changed, 26 insertions(+), 46 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/ArmorExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/ArmorExternal.java b/external-sop/src/main/java/sop/external/operation/ArmorExternal.java deleted file mode 100644 index e1d02e9..0000000 --- a/external-sop/src/main/java/sop/external/operation/ArmorExternal.java +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Ready; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.Armor; - -import javax.annotation.Nonnull; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link Armor} operation using an external SOP binary. - */ -public class ArmorExternal implements Armor { - - private final List commandList = new ArrayList<>(); - private final List envList; - - public ArmorExternal(String binary, Properties environment) { - commandList.add(binary); - commandList.add("armor"); - envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Deprecated - @Nonnull - public Armor label(@Nonnull ArmorLabel label) throws SOPGPException.UnsupportedOption { - commandList.add("--label=" + label); - return this; - } - - @Override - @Nonnull - public Ready data(@Nonnull InputStream data) throws SOPGPException.BadData { - return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data); - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt new file mode 100644 index 0000000..f80c57b --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +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 + +/** Implementation of the [Armor] operation using an external SOP binary. */ +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) +} From d149aac56ca40bf349989537e1cb19806ddf169f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 16:45:21 +0100 Subject: [PATCH 141/298] Kotlin conversion: ChangeKeyPasswordExternal --- .../operation/ChangeKeyPasswordExternal.java | 62 ------------------- .../operation/ChangeKeyPasswordExternal.kt | 37 +++++++++++ 2 files changed, 37 insertions(+), 62 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/ChangeKeyPasswordExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java b/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java deleted file mode 100644 index d53d6a5..0000000 --- a/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Ready; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.ChangeKeyPassword; - -import javax.annotation.Nonnull; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -public class ChangeKeyPasswordExternal implements ChangeKeyPassword { - private final List commandList = new ArrayList<>(); - private final List envList; - - private int keyPasswordCounter = 0; - - public ChangeKeyPasswordExternal(String binary, Properties environment) { - this.commandList.add(binary); - this.commandList.add("decrypt"); - this.envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public ChangeKeyPassword noArmor() { - this.commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public ChangeKeyPassword oldKeyPassphrase(@Nonnull String oldPassphrase) { - this.commandList.add("--old-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter); - this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + oldPassphrase); - keyPasswordCounter++; - - return this; - } - - @Override - @Nonnull - public ChangeKeyPassword newKeyPassphrase(@Nonnull String newPassphrase) { - this.commandList.add("--new-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter); - this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + newPassphrase); - keyPasswordCounter++; - - return this; - } - - @Override - @Nonnull - public Ready keys(@Nonnull InputStream inputStream) throws SOPGPException.KeyIsProtected, SOPGPException.BadData { - return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, inputStream); - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/ChangeKeyPasswordExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ChangeKeyPasswordExternal.kt new file mode 100644 index 0000000..a45e59f --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ChangeKeyPasswordExternal.kt @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.ChangeKeyPassword + +/** Implementation of the [ChangeKeyPassword] operation using an external SOP binary. */ +class ChangeKeyPasswordExternal(binary: String, environment: Properties) : ChangeKeyPassword { + + private val commandList: MutableList = mutableListOf(binary, "change-key-password") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var keyPasswordCounter = 0 + + override fun noArmor(): ChangeKeyPassword = apply { commandList.add("--no-armor") } + + override fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword = apply { + commandList.add("--old-key-password=@ENV:KEY_PASSWORD_$keyPasswordCounter") + envList.add("KEY_PASSWORD_$keyPasswordCounter=$oldPassphrase") + keyPasswordCounter += 1 + } + + override fun newKeyPassphrase(newPassphrase: String): ChangeKeyPassword = apply { + commandList.add("--new-key-password=@ENV:KEY_PASSWORD_$keyPasswordCounter") + envList.add("KEY_PASSWORD_$keyPasswordCounter=$newPassphrase") + keyPasswordCounter += 1 + } + + override fun keys(keys: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys) +} From da2b299f4d6bea493585e7640faa0d1616fd4edc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 16:45:32 +0100 Subject: [PATCH 142/298] Kotlin conversion: DearmorExternal --- .../external/operation/DearmorExternal.java | 37 ------------------- .../sop/external/operation/DearmorExternal.kt | 20 ++++++++++ 2 files changed, 20 insertions(+), 37 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/DearmorExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/DearmorExternal.java b/external-sop/src/main/java/sop/external/operation/DearmorExternal.java deleted file mode 100644 index cd3da6f..0000000 --- a/external-sop/src/main/java/sop/external/operation/DearmorExternal.java +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Ready; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.Dearmor; - -import javax.annotation.Nonnull; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link Dearmor} operation using an external SOP binary. - */ -public class DearmorExternal implements Dearmor { - - private final List commandList = new ArrayList<>(); - private final List envList; - - public DearmorExternal(String binary, Properties environment) { - commandList.add(binary); - commandList.add("dearmor"); - envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public Ready data(@Nonnull InputStream data) throws SOPGPException.BadData { - return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data); - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt new file mode 100644 index 0000000..928d9b4 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.Dearmor + +/** Implementation of the [Dearmor] operation using an external SOP binary. */ +class DearmorExternal(binary: String, environment: Properties) : Dearmor { + private val commandList = listOf(binary, "dearmor") + private val envList = ExternalSOP.propertiesToEnv(environment) + + override fun data(data: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) +} From 03da9bbfb705c67906c9904204b7375d7a382a7c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:04:00 +0100 Subject: [PATCH 143/298] Kotlin conversion: DecryptExternal --- .../external/operation/DecryptExternal.java | 185 ------------------ .../sop/external/operation/DecryptExternal.kt | 133 +++++++++++++ 2 files changed, 133 insertions(+), 185 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/DecryptExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/DecryptExternal.java b/external-sop/src/main/java/sop/external/operation/DecryptExternal.java deleted file mode 100644 index a1c4016..0000000 --- a/external-sop/src/main/java/sop/external/operation/DecryptExternal.java +++ /dev/null @@ -1,185 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.Verification; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.Decrypt; -import sop.util.UTCUtil; - -import javax.annotation.Nonnull; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link Decrypt} operation using an external SOP binary. - */ -public class DecryptExternal implements Decrypt { - - private final ExternalSOP.TempDirProvider tempDirProvider; - private final List commandList = new ArrayList<>(); - private final List envList; - - private int verifyWithCounter = 0; - private int withSessionKeyCounter = 0; - private int withPasswordCounter = 0; - private int keyCounter = 0; - private int withKeyPasswordCounter = 0; - - public DecryptExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) { - this.tempDirProvider = tempDirProvider; - this.commandList.add(binary); - this.commandList.add("decrypt"); - this.envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public Decrypt verifyNotBefore(@Nonnull Date timestamp) - throws SOPGPException.UnsupportedOption { - this.commandList.add("--verify-not-before=" + UTCUtil.formatUTCDate(timestamp)); - return this; - } - - @Override - @Nonnull - public Decrypt verifyNotAfter(@Nonnull Date timestamp) - throws SOPGPException.UnsupportedOption { - this.commandList.add("--verify-not-after=" + UTCUtil.formatUTCDate(timestamp)); - return this; - } - - @Override - @Nonnull - public Decrypt verifyWithCert(@Nonnull InputStream cert) - throws SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { - String envVar = "VERIFY_WITH_" + verifyWithCounter++; - commandList.add("--verify-with=@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(cert)); - return this; - } - - @Override - @Nonnull - public Decrypt withSessionKey(@Nonnull SessionKey sessionKey) - throws SOPGPException.UnsupportedOption { - String envVar = "SESSION_KEY_" + withSessionKeyCounter++; - commandList.add("--with-session-key=@ENV:" + envVar); - envList.add(envVar + "=" + sessionKey); - return this; - } - - @Override - @Nonnull - public Decrypt withPassword(@Nonnull String password) - throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - String envVar = "PASSWORD_" + withPasswordCounter++; - commandList.add("--with-password=@ENV:" + envVar); - envList.add(envVar + "=" + password); - return this; - } - - @Override - @Nonnull - public Decrypt withKey(@Nonnull InputStream key) - throws SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { - String envVar = "KEY_" + keyCounter++; - commandList.add("@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(key)); - return this; - } - - @Override - @Nonnull - public Decrypt withKeyPassword(@Nonnull byte[] password) - throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { - String envVar = "KEY_PASSWORD_" + withKeyPasswordCounter++; - commandList.add("--with-key-password=@ENV:" + envVar); - envList.add(envVar + "=" + new String(password)); - return this; - } - - @Override - @Nonnull - public ReadyWithResult ciphertext(@Nonnull InputStream ciphertext) - throws SOPGPException.BadData, SOPGPException.MissingArg, SOPGPException.CannotDecrypt, - SOPGPException.KeyIsProtected, IOException { - File tempDir = tempDirProvider.provideTempDirectory(); - - File sessionKeyOut = new File(tempDir, "session-key-out"); - sessionKeyOut.delete(); - commandList.add("--session-key-out=" + sessionKeyOut.getAbsolutePath()); - - File verifyOut = new File(tempDir, "verifications-out"); - verifyOut.delete(); - if (verifyWithCounter != 0) { - commandList.add("--verify-out=" + verifyOut.getAbsolutePath()); - } - - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - try { - Process process = Runtime.getRuntime().exec(command, env); - OutputStream processOut = process.getOutputStream(); - InputStream processIn = process.getInputStream(); - - return new ReadyWithResult() { - @Override - public DecryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException { - byte[] buf = new byte[4096]; - int r; - while ((r = ciphertext.read(buf)) > 0) { - processOut.write(buf, 0, r); - } - - ciphertext.close(); - processOut.close(); - - while ((r = processIn.read(buf)) > 0) { - outputStream.write(buf, 0 , r); - } - - processIn.close(); - outputStream.close(); - - ExternalSOP.finish(process); - - FileInputStream sessionKeyOutIn = new FileInputStream(sessionKeyOut); - String line = ExternalSOP.readString(sessionKeyOutIn); - SessionKey sessionKey = SessionKey.fromString(line.trim()); - sessionKeyOutIn.close(); - sessionKeyOut.delete(); - - List verifications = new ArrayList<>(); - if (verifyWithCounter != 0) { - FileInputStream verifyOutIn = new FileInputStream(verifyOut); - BufferedReader reader = new BufferedReader(new InputStreamReader(verifyOutIn)); - while ((line = reader.readLine()) != null) { - verifications.add(Verification.fromString(line.trim())); - } - reader.close(); - } - - return new DecryptionResult(sessionKey, verifications); - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt new file mode 100644 index 0000000..b68d3a6 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.* +import java.util.* +import sop.DecryptionResult +import sop.ReadyWithResult +import sop.SessionKey +import sop.Verification +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.external.ExternalSOP.Companion.readString +import sop.operation.Decrypt +import sop.util.UTCUtil + +/** Implementation of the [Decrypt] operation using an external SOP binary. */ +class DecryptExternal( + binary: String, + environment: Properties, + private val tempDirProvider: ExternalSOP.TempDirProvider +) : Decrypt { + + private val commandList = mutableListOf(binary, "decrypt") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + private var requireVerification = false + + override fun verifyNotBefore(timestamp: Date): Decrypt = apply { + commandList.add("--verify-not-before=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun verifyNotAfter(timestamp: Date): Decrypt = apply { + commandList.add("--verify-not-after=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun verifyWithCert(cert: InputStream): Decrypt = apply { + commandList.add("--verify-with=@ENV:VERIFY_WITH_$argCounter") + envList.add("VERIFY_WITH_$argCounter=${readString(cert)}") + argCounter += 1 + requireVerification = true + } + + override fun withSessionKey(sessionKey: SessionKey): Decrypt = apply { + commandList.add("--with-session-key=@ENV:SESSION_KEY_$argCounter") + envList.add("SESSION_KEY_$argCounter=$sessionKey") + argCounter += 1 + } + + override fun withPassword(password: String): Decrypt = apply { + commandList.add("--with-password=@ENV:PASSWORD_$argCounter") + envList.add("PASSWORD_$argCounter=$password") + argCounter += 1 + } + + override fun withKey(key: InputStream): Decrypt = apply { + commandList.add("@ENV:KEY_$argCounter") + envList.add("KEY_$argCounter=${readString(key)}") + argCounter += 1 + } + + override fun withKeyPassword(password: ByteArray): Decrypt = apply { + commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCounter") + envList.add("KEY_PASSWORD_$argCounter=${String(password)}") + argCounter += 1 + } + + override fun ciphertext(ciphertext: InputStream): ReadyWithResult { + val tempDir = tempDirProvider.provideTempDirectory() + + val sessionKeyOut = File(tempDir, "session-key-out") + sessionKeyOut.delete() + commandList.add("--session-key-out=${sessionKeyOut.absolutePath}") + + val verifyOut = File(tempDir, "verifications-out") + verifyOut.delete() + if (requireVerification) { + commandList.add("--verify-out=${verifyOut.absolutePath}") + } + + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): DecryptionResult { + val buf = ByteArray(4096) + var r: Int + while (ciphertext.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + ciphertext.close() + processOut.close() + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + + processIn.close() + outputStream.close() + + finish(process) + + val sessionKeyOutIn = FileInputStream(sessionKeyOut) + var line = readString(sessionKeyOutIn) + val sessionKey = SessionKey.fromString(line.trim { it <= ' ' }) + sessionKeyOutIn.close() + sessionKeyOut.delete() + + val verifications: MutableList = ArrayList() + if (requireVerification) { + val verifyOutIn = FileInputStream(verifyOut) + val reader = BufferedReader(InputStreamReader(verifyOutIn)) + while (reader.readLine().also { line = it } != null) { + verifications.add(Verification.fromString(line.trim())) + } + reader.close() + } + + return DecryptionResult(sessionKey, verifications) + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 9cd9f151c9b6138b3e2fe5999efea347922ee9ae Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:13:28 +0100 Subject: [PATCH 144/298] Kotlin conversion: DetachedSignExternal --- .../operation/DetachedSignExternal.java | 142 ------------------ .../operation/DetachedSignExternal.kt | 104 +++++++++++++ 2 files changed, 104 insertions(+), 142 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/DetachedSignExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java b/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java deleted file mode 100644 index 2ef2714..0000000 --- a/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java +++ /dev/null @@ -1,142 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.MicAlg; -import sop.ReadyWithResult; -import sop.SigningResult; -import sop.enums.SignAs; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.DetachedSign; - -import javax.annotation.Nonnull; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link DetachedSign} operation using an external SOP binary. - */ -public class DetachedSignExternal implements DetachedSign { - - private final ExternalSOP.TempDirProvider tempDirProvider; - private final List commandList = new ArrayList<>(); - private final List envList; - - private int withKeyPasswordCounter = 0; - private int keyCounter = 0; - - public DetachedSignExternal(String binary, Properties properties, ExternalSOP.TempDirProvider tempDirProvider) { - this.tempDirProvider = tempDirProvider; - commandList.add(binary); - commandList.add("sign"); - envList = ExternalSOP.propertiesToEnv(properties); - } - - @Override - @Nonnull - public DetachedSign noArmor() { - commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public DetachedSign key(@Nonnull InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { - String envVar = "KEY_" + keyCounter++; - commandList.add("@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(key)); - return this; - } - - @Override - @Nonnull - public DetachedSign withKeyPassword(@Nonnull byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { - String envVar = "WITH_KEY_PASSWORD_" + withKeyPasswordCounter++; - commandList.add("--with-key-password=@ENV:" + envVar); - envList.add(envVar + "=" + new String(password)); - return this; - } - - @Override - @Nonnull - public DetachedSign mode(@Nonnull SignAs mode) throws SOPGPException.UnsupportedOption { - commandList.add("--as=" + mode); - return this; - } - - @Override - @Nonnull - public ReadyWithResult data(@Nonnull InputStream data) - throws IOException, SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { - - File tempDir = tempDirProvider.provideTempDirectory(); - File micAlgOut = new File(tempDir, "micAlgOut"); - micAlgOut.delete(); - commandList.add("--micalg-out=" + micAlgOut.getAbsolutePath()); - - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - try { - Process process = Runtime.getRuntime().exec(command, env); - OutputStream processOut = process.getOutputStream(); - InputStream processIn = process.getInputStream(); - - return new ReadyWithResult() { - @Override - public SigningResult writeTo(@Nonnull OutputStream outputStream) throws IOException { - byte[] buf = new byte[4096]; - int r; - while ((r = data.read(buf)) > 0) { - processOut.write(buf, 0, r); - } - - data.close(); - try { - processOut.close(); - } catch (IOException e) { - // Ignore Stream closed - if (!"Stream closed".equals(e.getMessage())) { - throw e; - } - } - - while ((r = processIn.read(buf)) > 0) { - outputStream.write(buf, 0 , r); - } - - processIn.close(); - outputStream.close(); - - ExternalSOP.finish(process); - - SigningResult.Builder builder = SigningResult.builder(); - if (micAlgOut.exists()) { - BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(micAlgOut))); - String line = reader.readLine(); - if (line != null && !line.trim().isEmpty()) { - MicAlg micAlg = new MicAlg(line.trim()); - builder.setMicAlg(micAlg); - } - reader.close(); - micAlgOut.delete(); - } - - return builder.build(); - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/DetachedSignExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DetachedSignExternal.kt new file mode 100644 index 0000000..66d1db8 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/DetachedSignExternal.kt @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.* +import java.util.* +import sop.MicAlg +import sop.ReadyWithResult +import sop.SigningResult +import sop.SigningResult.Companion.builder +import sop.enums.SignAs +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.operation.DetachedSign + +/** Implementation of the [DetachedSign] operation using an external SOP binary. */ +class DetachedSignExternal( + binary: String, + environment: Properties, + private val tempDirProvider: ExternalSOP.TempDirProvider +) : DetachedSign { + + private val commandList = mutableListOf(binary, "sign") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + + override fun mode(mode: SignAs): DetachedSign = apply { commandList.add("--as=$mode") } + + override fun data(data: InputStream): ReadyWithResult { + val tempDir = tempDirProvider.provideTempDirectory() + val micAlgOut = File(tempDir, "micAlgOut") + micAlgOut.delete() + commandList.add("--micalg-out=${micAlgOut.absolutePath}") + + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): SigningResult { + val buf = ByteArray(4096) + var r: Int + while (data.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + data.close() + try { + processOut.close() + } catch (e: IOException) { + // Ignore Stream closed + if ("Stream closed" != e.message) { + throw e + } + } + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + + processIn.close() + outputStream.close() + + finish(process) + + val builder = builder() + if (micAlgOut.exists()) { + val reader = BufferedReader(InputStreamReader(FileInputStream(micAlgOut))) + val line = reader.readLine() + if (line != null && line.isNotBlank()) { + val micAlg = MicAlg(line.trim()) + builder.setMicAlg(micAlg) + } + reader.close() + micAlgOut.delete() + } + + return builder.build() + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + override fun noArmor(): DetachedSign = apply { commandList.add("--no-armor") } + + override fun key(key: InputStream): DetachedSign = apply { + commandList.add("@ENV:KEY_$argCounter") + envList.add("KEY_$argCounter=${ExternalSOP.readString(key)}") + argCounter += 1 + } + + override fun withKeyPassword(password: ByteArray): DetachedSign = apply { + commandList.add("--with-key-password=@ENV:WITH_KEY_PASSWORD_$argCounter") + envList.add("WITH_KEY_PASSWORD_$argCounter=${String(password)}") + argCounter += 1 + } +} From 4a405f6d394d95a5b343b5e42fc00334d0fceef3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:22:07 +0100 Subject: [PATCH 145/298] Kotlin conversion: DetachedVerifyExternal --- .../operation/DetachedVerifyExternal.java | 117 ------------------ .../operation/DetachedVerifyExternal.kt | 90 ++++++++++++++ 2 files changed, 90 insertions(+), 117 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java b/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java deleted file mode 100644 index 866150d..0000000 --- a/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Verification; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.DetachedVerify; -import sop.operation.VerifySignatures; -import sop.util.UTCUtil; - -import javax.annotation.Nonnull; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Properties; -import java.util.Set; - -/** - * Implementation of the {@link DetachedVerify} operation using an external SOP binary. - */ -public class DetachedVerifyExternal implements DetachedVerify { - - private final List commandList = new ArrayList<>(); - private final List envList; - - private final Set certs = new HashSet<>(); - private InputStream signatures; - private int certCounter = 0; - - public DetachedVerifyExternal(String binary, Properties environment) { - commandList.add(binary); - commandList.add("verify"); - envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public DetachedVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - commandList.add("--not-before=" + UTCUtil.formatUTCDate(timestamp)); - return this; - } - - @Override - @Nonnull - public DetachedVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - commandList.add("--not-after=" + UTCUtil.formatUTCDate(timestamp)); - return this; - } - - @Override - @Nonnull - public DetachedVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData { - this.certs.add(cert); - return this; - } - - @Override - @Nonnull - public VerifySignatures signatures(@Nonnull InputStream signatures) throws SOPGPException.BadData { - this.signatures = signatures; - return this; - } - - @Override - @Nonnull - public List data(@Nonnull InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { - commandList.add("@ENV:SIGNATURE"); - envList.add("SIGNATURE=" + ExternalSOP.readString(signatures)); - - for (InputStream cert : certs) { - String envVar = "CERT_" + certCounter++; - commandList.add("@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(cert)); - } - - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - - try { - Process process = Runtime.getRuntime().exec(command, env); - OutputStream processOut = process.getOutputStream(); - InputStream processIn = process.getInputStream(); - - byte[] buf = new byte[4096]; - int r; - while ((r = data.read(buf)) > 0) { - processOut.write(buf, 0, r); - } - - data.close(); - processOut.close(); - - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(processIn)); - List verifications = new ArrayList<>(); - - String line = null; - while ((line = bufferedReader.readLine()) != null) { - verifications.add(Verification.fromString(line)); - } - - ExternalSOP.finish(process); - - return verifications; - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt new file mode 100644 index 0000000..3340a33 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.util.* +import sop.Verification +import sop.Verification.Companion.fromString +import sop.exception.SOPGPException +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.operation.DetachedVerify +import sop.operation.VerifySignatures +import sop.util.UTCUtil + +/** Implementation of the [DetachedVerify] operation using an external SOP binary. */ +class DetachedVerifyExternal(binary: String, environment: Properties) : DetachedVerify { + + private val commandList = mutableListOf(binary, "verify") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var signatures: InputStream? = null + private val certs: MutableSet = mutableSetOf() + private var argCounter = 0 + + override fun signatures(signatures: InputStream): VerifySignatures = apply { + this.signatures = signatures + } + + override fun notBefore(timestamp: Date): DetachedVerify = apply { + commandList.add("--not-before=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun notAfter(timestamp: Date): DetachedVerify = apply { + commandList.add("--not-after=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun cert(cert: InputStream): DetachedVerify = apply { this.certs.add(cert) } + + override fun data(data: InputStream): List { + // Signature + if (signatures == null) { + throw SOPGPException.MissingArg("Missing argument: signatures cannot be null.") + } + commandList.add("@ENV:SIGNATURE") + envList.add("SIGNATURE=${ExternalSOP.readString(signatures!!)}") + + // Certs + for (cert in certs) { + commandList.add("@ENV:CERT_$argCounter") + envList.add("CERT_$argCounter=${ExternalSOP.readString(cert)}") + argCounter += 1 + } + + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + val buf = ByteArray(4096) + var r: Int + while (data.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + data.close() + processOut.close() + + val bufferedReader = BufferedReader(InputStreamReader(processIn)) + val verifications: MutableList = ArrayList() + + var line: String? + while (bufferedReader.readLine().also { line = it } != null) { + verifications.add(fromString(line!!)) + } + + finish(process) + + return verifications + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From c53c69f3acb503f01bcf75d2f0601ba51c4c243a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:32:43 +0100 Subject: [PATCH 146/298] Kotlin conversion: EncryptExternal --- .../external/operation/EncryptExternal.java | 160 ------------------ .../sop/external/operation/EncryptExternal.kt | 111 ++++++++++++ 2 files changed, 111 insertions(+), 160 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/EncryptExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java deleted file mode 100644 index f41a36e..0000000 --- a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java +++ /dev/null @@ -1,160 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.EncryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.Encrypt; - -import javax.annotation.Nonnull; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link Encrypt} operation using an external SOP binary. - */ -public class EncryptExternal implements Encrypt { - - private final ExternalSOP.TempDirProvider tempDirProvider; - private final List commandList = new ArrayList<>(); - private final List envList; - private int SIGN_WITH_COUNTER = 0; - private int KEY_PASSWORD_COUNTER = 0; - private int PASSWORD_COUNTER = 0; - private int CERT_COUNTER = 0; - - public EncryptExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) { - this.tempDirProvider = tempDirProvider; - this.commandList.add(binary); - this.commandList.add("encrypt"); - this.envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public Encrypt noArmor() { - this.commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public Encrypt mode(@Nonnull EncryptAs mode) - throws SOPGPException.UnsupportedOption { - this.commandList.add("--as=" + mode); - return this; - } - - @Override - @Nonnull - public Encrypt signWith(@Nonnull InputStream key) - throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, - IOException { - String envVar = "SIGN_WITH_" + SIGN_WITH_COUNTER++; - commandList.add("--sign-with=@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(key)); - return this; - } - - @Override - @Nonnull - public Encrypt withKeyPassword(@Nonnull byte[] password) - throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - String envVar = "KEY_PASSWORD_" + KEY_PASSWORD_COUNTER++; - commandList.add("--with-key-password=@ENV:" + envVar); - envList.add(envVar + "=" + new String(password)); - return this; - } - - @Override - @Nonnull - public Encrypt withPassword(@Nonnull String password) - throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - String envVar = "PASSWORD_" + PASSWORD_COUNTER++; - commandList.add("--with-password=@ENV:" + envVar); - envList.add(envVar + "=" + password); - return this; - } - - @Override - @Nonnull - public Encrypt withCert(@Nonnull InputStream cert) - throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, - IOException { - String envVar = "CERT_" + CERT_COUNTER++; - commandList.add("@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(cert)); - return this; - } - - @Override - @Nonnull - public Encrypt profile(@Nonnull String profileName) { - commandList.add("--profile=" + profileName); - return this; - } - - @Override - @Nonnull - public ReadyWithResult plaintext(@Nonnull InputStream plaintext) - throws SOPGPException.KeyIsProtected, IOException { - File tempDir = tempDirProvider.provideTempDirectory(); - - File sessionKeyOut = new File(tempDir, "session-key-out"); - sessionKeyOut.delete(); - commandList.add("--session-key-out=" + sessionKeyOut.getAbsolutePath()); - - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - try { - Process process = Runtime.getRuntime().exec(command, env); - OutputStream processOut = process.getOutputStream(); - InputStream processIn = process.getInputStream(); - - return new ReadyWithResult() { - @Override - public EncryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException { - byte[] buf = new byte[4096]; - int r; - while ((r = plaintext.read(buf)) > 0) { - processOut.write(buf, 0, r); - } - - plaintext.close(); - processOut.close(); - - while ((r = processIn.read(buf)) > 0) { - outputStream.write(buf, 0 , r); - } - - processIn.close(); - outputStream.close(); - - ExternalSOP.finish(process); - - FileInputStream sessionKeyOutIn = new FileInputStream(sessionKeyOut); - String line = ExternalSOP.readString(sessionKeyOutIn); - SessionKey sessionKey = SessionKey.fromString(line.trim()); - sessionKeyOutIn.close(); - sessionKeyOut.delete(); - - return new EncryptionResult(sessionKey); - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt new file mode 100644 index 0000000..6f1cc6c --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.* +import sop.EncryptionResult +import sop.ReadyWithResult +import sop.SessionKey.Companion.fromString +import sop.enums.EncryptAs +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.external.ExternalSOP.Companion.readString +import sop.operation.Encrypt + +/** Implementation of the [Encrypt] operation using an external SOP binary. */ +class EncryptExternal( + binary: String, + environment: Properties, + private val tempDirProvider: ExternalSOP.TempDirProvider +) : Encrypt { + + private val commandList = mutableListOf(binary, "encrypt") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + + override fun noArmor(): Encrypt = apply { commandList.add("--no-armor") } + + 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") + envList.add("SIGN_WITH_$argCounter=${ExternalSOP.readString(key)}") + argCounter += 1 + } + + override fun withKeyPassword(password: ByteArray): Encrypt = apply { + commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCounter") + envList.add("KEY_PASSWORD_$argCounter=${String(password)}") + argCounter += 1 + } + + override fun withPassword(password: String): Encrypt = apply { + commandList.add("--with-password=@ENV:PASSWORD_$argCounter") + envList.add("PASSWORD_$argCounter=$password") + argCounter += 1 + } + + override fun withCert(cert: InputStream): Encrypt = apply { + commandList.add("@ENV:CERT_$argCounter") + envList.add("CERT_$argCounter=${readString(cert)}") + argCounter += 1 + } + + override fun profile(profileName: String): Encrypt = apply { + commandList.add("--profile=$profileName") + } + + override fun plaintext(plaintext: InputStream): ReadyWithResult { + val tempDir = tempDirProvider.provideTempDirectory() + + val sessionKeyOut = File(tempDir, "session-key-out") + sessionKeyOut.delete() + commandList.add("--session-key-out=${sessionKeyOut.absolutePath}") + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): EncryptionResult { + val buf = ByteArray(4096) + var r: Int + while (plaintext.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + plaintext.close() + processOut.close() + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + + processIn.close() + outputStream.close() + + finish(process) + + val sessionKeyOutIn = FileInputStream(sessionKeyOut) + val line = readString(sessionKeyOutIn) + val sessionKey = fromString(line.trim()) + sessionKeyOutIn.close() + sessionKeyOut.delete() + + return EncryptionResult(sessionKey) + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 01abae4d080d47660d4c881ef2fe6bf2bdc26bd5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:36:00 +0100 Subject: [PATCH 147/298] Kotlin conversion: ExtractCertExternal --- .../operation/ExtractCertExternal.java | 44 ------------------- .../external/operation/ExtractCertExternal.kt | 24 ++++++++++ 2 files changed, 24 insertions(+), 44 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/ExtractCertExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java b/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java deleted file mode 100644 index 538359a..0000000 --- a/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Ready; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.ExtractCert; - -import javax.annotation.Nonnull; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link ExtractCert} operation using an external SOP binary. - */ -public class ExtractCertExternal implements ExtractCert { - - private final List commandList = new ArrayList<>(); - private final List envList; - - public ExtractCertExternal(String binary, Properties properties) { - this.commandList.add(binary); - this.commandList.add("extract-cert"); - this.envList = ExternalSOP.propertiesToEnv(properties); - } - - @Override - @Nonnull - public ExtractCert noArmor() { - this.commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public Ready key(@Nonnull InputStream keyInputStream) throws SOPGPException.BadData { - return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keyInputStream); - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/ExtractCertExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ExtractCertExternal.kt new file mode 100644 index 0000000..9b86733 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ExtractCertExternal.kt @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.ExtractCert + +/** Implementation of the [ExtractCert] operation using an external SOP binary. */ +class ExtractCertExternal(binary: String, environment: Properties) : ExtractCert { + + private val commandList = mutableListOf(binary, "extract-cert") + private val envList = ExternalSOP.propertiesToEnv(environment) + + override fun noArmor(): ExtractCert = apply { commandList.add("--no-armor") } + + override fun key(keyInputStream: InputStream): Ready = + ExternalSOP.executeTransformingOperation( + Runtime.getRuntime(), commandList, envList, keyInputStream) +} From 9b79a49bb54a7a5d5ef588394f9f63dc20e463e1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:41:33 +0100 Subject: [PATCH 148/298] Kotlin conversion: GenerateKeyExternal --- .../operation/GenerateKeyExternal.java | 78 ------------------- .../external/operation/GenerateKeyExternal.kt | 38 +++++++++ 2 files changed, 38 insertions(+), 78 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java b/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java deleted file mode 100644 index a8244cb..0000000 --- a/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Ready; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.GenerateKey; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link GenerateKey} operation using an external SOP binary. - */ -public class GenerateKeyExternal implements GenerateKey { - - private final List commandList = new ArrayList<>(); - private final List envList; - - private int keyPasswordCounter = 0; - - public GenerateKeyExternal(String binary, Properties environment) { - this.commandList.add(binary); - this.commandList.add("generate-key"); - this.envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public GenerateKey noArmor() { - this.commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public GenerateKey userId(@Nonnull String userId) { - this.commandList.add(userId); - return this; - } - - @Override - @Nonnull - public GenerateKey withKeyPassword(@Nonnull String password) - throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - this.commandList.add("--with-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter); - this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + password); - keyPasswordCounter++; - - return this; - } - - @Override - @Nonnull - public GenerateKey profile(@Nonnull String profile) { - commandList.add("--profile=" + profile); - return this; - } - - @Override - @Nonnull - public GenerateKey signingOnly() { - commandList.add("--signing-only"); - return this; - } - - @Override - @Nonnull - public Ready generate() - throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { - return ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList); - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt new file mode 100644 index 0000000..37116a4 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.util.Properties +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.GenerateKey + +/** Implementation of the [GenerateKey] operation using an external SOP binary. */ +class GenerateKeyExternal(binary: String, environment: Properties) : GenerateKey { + + private val commandList = mutableListOf(binary, "generate-key") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + + override fun noArmor(): GenerateKey = apply { commandList.add("--no-armor") } + + override fun userId(userId: String): GenerateKey = apply { commandList.add(userId) } + + override fun withKeyPassword(password: String): GenerateKey = apply { + commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCounter") + envList.add("KEY_PASSWORD_$argCounter=$password") + argCounter += 1 + } + + override fun profile(profile: String): GenerateKey = apply { + commandList.add("--profile=$profile") + } + + override fun signingOnly(): GenerateKey = apply { commandList.add("--signing-only") } + + override fun generate(): Ready = + ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList) +} From f1814530041462e65cb137dda085893d03a6a889 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:47:37 +0100 Subject: [PATCH 149/298] Kotlin conversion: InlineDetachExternal --- .../operation/InlineDetachExternal.java | 106 ------------------ .../operation/InlineDetachExternal.kt | 82 ++++++++++++++ 2 files changed, 82 insertions(+), 106 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java b/external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java deleted file mode 100644 index a798006..0000000 --- a/external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.ReadyWithResult; -import sop.Signatures; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.InlineDetach; - -import javax.annotation.Nonnull; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link InlineDetach} operation using an external SOP binary. - */ -public class InlineDetachExternal implements InlineDetach { - - private final ExternalSOP.TempDirProvider tempDirProvider; - private final List commandList = new ArrayList<>(); - private final List envList; - - public InlineDetachExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) { - this.tempDirProvider = tempDirProvider; - commandList.add(binary); - commandList.add("inline-detach"); - envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public InlineDetach noArmor() { - commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public ReadyWithResult message(@Nonnull InputStream messageInputStream) throws IOException, SOPGPException.BadData { - File tempDir = tempDirProvider.provideTempDirectory(); - - File signaturesOut = new File(tempDir, "signatures"); - signaturesOut.delete(); - commandList.add("--signatures-out=" + signaturesOut.getAbsolutePath()); - - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - - try { - Process process = Runtime.getRuntime().exec(command, env); - OutputStream processOut = process.getOutputStream(); - InputStream processIn = process.getInputStream(); - - return new ReadyWithResult() { - @Override - public Signatures writeTo(@Nonnull OutputStream outputStream) throws IOException { - byte[] buf = new byte[4096]; - int r; - while ((r = messageInputStream.read(buf)) > 0) { - processOut.write(buf, 0, r); - } - - messageInputStream.close(); - processOut.close(); - - while ((r = processIn.read(buf)) > 0) { - outputStream.write(buf, 0 , r); - } - - processIn.close(); - outputStream.close(); - - ExternalSOP.finish(process); - - FileInputStream signaturesOutIn = new FileInputStream(signaturesOut); - ByteArrayOutputStream signaturesBuffer = new ByteArrayOutputStream(); - while ((r = signaturesOutIn.read(buf)) > 0) { - signaturesBuffer.write(buf, 0, r); - } - signaturesOutIn.close(); - signaturesOut.delete(); - - final byte[] sigBytes = signaturesBuffer.toByteArray(); - return new Signatures() { - @Override - public void writeTo(@Nonnull OutputStream signatureOutputStream) throws IOException { - signatureOutputStream.write(sigBytes); - } - }; - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt new file mode 100644 index 0000000..f44e6bb --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.* +import java.util.* +import sop.ReadyWithResult +import sop.Signatures +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.operation.InlineDetach + +/** Implementation of the [InlineDetach] operation using an external SOP binary. */ +class InlineDetachExternal( + binary: String, + environment: Properties, + private val tempDirProvider: ExternalSOP.TempDirProvider +) : InlineDetach { + + private val commandList = mutableListOf(binary, "inline-detach") + private val envList = ExternalSOP.propertiesToEnv(environment) + + override fun noArmor(): InlineDetach = apply { commandList.add("--no-armor") } + + override fun message(messageInputStream: InputStream): ReadyWithResult { + val tempDir = tempDirProvider.provideTempDirectory() + + val signaturesOut = File(tempDir, "signatures") + signaturesOut.delete() + commandList.add("--signatures-out=${signaturesOut.absolutePath}") + + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): Signatures { + val buf = ByteArray(4096) + var r: Int + while (messageInputStream.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + messageInputStream.close() + processOut.close() + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + + processIn.close() + outputStream.close() + + finish(process) + + val signaturesOutIn = FileInputStream(signaturesOut) + val signaturesBuffer = ByteArrayOutputStream() + while (signaturesOutIn.read(buf).also { r = it } > 0) { + signaturesBuffer.write(buf, 0, r) + } + signaturesOutIn.close() + signaturesOut.delete() + + val sigBytes = signaturesBuffer.toByteArray() + + return object : Signatures() { + @Throws(IOException::class) + override fun writeTo(outputStream: OutputStream) { + outputStream.write(sigBytes) + } + } + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 7be71494cfdc063d7e370c7305bd017061532e86 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:52:56 +0100 Subject: [PATCH 150/298] Kotlin conversion: InlineSignExternal --- .../operation/InlineSignExternal.java | 74 ------------------- .../external/operation/InlineSignExternal.kt | 40 ++++++++++ 2 files changed, 40 insertions(+), 74 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/InlineSignExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java b/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java deleted file mode 100644 index 1a86002..0000000 --- a/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Ready; -import sop.enums.InlineSignAs; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.InlineSign; - -import javax.annotation.Nonnull; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link InlineSign} operation using an external SOP binary. - */ -public class InlineSignExternal implements InlineSign { - - private final List commandList = new ArrayList<>(); - private final List envList; - - private int keyCounter = 0; - private int withKeyPasswordCounter = 0; - - public InlineSignExternal(String binary, Properties environment) { - commandList.add(binary); - commandList.add("inline-sign"); - envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public InlineSign noArmor() { - commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public InlineSign key(@Nonnull InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { - String envVar = "KEY_" + keyCounter++; - commandList.add("@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(key)); - return this; - } - - @Override - @Nonnull - public InlineSign withKeyPassword(@Nonnull byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { - String envVar = "WITH_KEY_PASSWORD_" + withKeyPasswordCounter++; - commandList.add("--with-key-password=@ENV:" + envVar); - envList.add(envVar + "=" + new String(password)); - return this; - } - - @Override - @Nonnull - public InlineSign mode(@Nonnull InlineSignAs mode) throws SOPGPException.UnsupportedOption { - commandList.add("--as=" + mode); - return this; - } - - @Override - @Nonnull - public Ready data(@Nonnull InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { - return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data); - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt new file mode 100644 index 0000000..a304e85 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.enums.InlineSignAs +import sop.external.ExternalSOP +import sop.operation.InlineSign + +/** Implementation of the [InlineSign] operation using an external SOP binary. */ +class InlineSignExternal(binary: String, environment: Properties) : InlineSign { + + private val commandList = mutableListOf(binary, "inline-sign") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + + override fun mode(mode: InlineSignAs): InlineSign = apply { commandList.add("--as=$mode") } + + override fun data(data: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) + + override fun noArmor(): InlineSign = apply { commandList.add("--no-armor") } + + override fun key(key: InputStream): InlineSign = apply { + commandList.add("@ENV:KEY_$argCounter") + envList.add("KEY_$argCounter=${ExternalSOP.readString(key)}") + argCounter += 1 + } + + override fun withKeyPassword(password: ByteArray): InlineSign = apply { + commandList.add("--with-key-password=@ENV:WITH_KEY_PASSWORD_$argCounter") + envList.add("WITH_KEY_PASSWORD_$argCounter=${String(password)}") + argCounter += 1 + } +} From 8dc51b67a3e58a768000f38967eafeef4c05a913 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:59:04 +0100 Subject: [PATCH 151/298] Kotlin conversion: InlineVerifyExternal --- .../operation/InlineVerifyExternal.java | 122 ------------------ .../operation/InlineVerifyExternal.kt | 91 +++++++++++++ 2 files changed, 91 insertions(+), 122 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java b/external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java deleted file mode 100644 index 10a2d47..0000000 --- a/external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java +++ /dev/null @@ -1,122 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.ReadyWithResult; -import sop.Verification; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.InlineVerify; -import sop.util.UTCUtil; - -import javax.annotation.Nonnull; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link InlineVerify} operation using an external SOP binary. - */ -public class InlineVerifyExternal implements InlineVerify { - - private final ExternalSOP.TempDirProvider tempDirProvider; - private final List commandList = new ArrayList<>(); - private final List envList; - - private int certCounter = 0; - - public InlineVerifyExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) { - this.tempDirProvider = tempDirProvider; - commandList.add(binary); - commandList.add("inline-verify"); - envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public InlineVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - commandList.add("--not-before=" + UTCUtil.formatUTCDate(timestamp)); - return this; - } - - @Override - @Nonnull - public InlineVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - commandList.add("--not-after=" + UTCUtil.formatUTCDate(timestamp)); - return this; - } - - @Override - @Nonnull - public InlineVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData, IOException { - String envVar = "CERT_" + certCounter++; - commandList.add("@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(cert)); - return this; - } - - @Override - @Nonnull - public ReadyWithResult> data(@Nonnull InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { - File tempDir = tempDirProvider.provideTempDirectory(); - - File verificationsOut = new File(tempDir, "verifications-out"); - verificationsOut.delete(); - commandList.add("--verifications-out=" + verificationsOut.getAbsolutePath()); - - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - - try { - Process process = Runtime.getRuntime().exec(command, env); - OutputStream processOut = process.getOutputStream(); - InputStream processIn = process.getInputStream(); - - return new ReadyWithResult>() { - @Override - public List writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature { - byte[] buf = new byte[4096]; - int r; - while ((r = data.read(buf)) > 0) { - processOut.write(buf, 0, r); - } - - data.close(); - processOut.close(); - - - while ((r = processIn.read(buf)) > 0) { - outputStream.write(buf, 0 , r); - } - - processIn.close(); - outputStream.close(); - - ExternalSOP.finish(process); - - FileInputStream verificationsOutIn = new FileInputStream(verificationsOut); - BufferedReader reader = new BufferedReader(new InputStreamReader(verificationsOutIn)); - List verificationList = new ArrayList<>(); - String line; - while ((line = reader.readLine()) != null) { - verificationList.add(Verification.fromString(line.trim())); - } - - return verificationList; - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt new file mode 100644 index 0000000..ed769e3 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.* +import java.util.* +import sop.ReadyWithResult +import sop.Verification +import sop.Verification.Companion.fromString +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.operation.InlineVerify +import sop.util.UTCUtil + +/** Implementation of the [InlineVerify] operation using an external SOP binary. */ +class InlineVerifyExternal( + binary: String, + environment: Properties, + private val tempDirProvider: ExternalSOP.TempDirProvider +) : InlineVerify { + + private val commandList = mutableListOf(binary, "inline-verify") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + + override fun data(data: InputStream): ReadyWithResult> { + val tempDir = tempDirProvider.provideTempDirectory() + + val verificationsOut = File(tempDir, "verifications-out") + verificationsOut.delete() + commandList.add("--verifications-out=${verificationsOut.absolutePath}") + + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : ReadyWithResult>() { + override fun writeTo(outputStream: OutputStream): List { + val buf = ByteArray(4096) + var r: Int + while (data.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + data.close() + processOut.close() + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + + processIn.close() + outputStream.close() + + finish(process) + + val verificationsOutIn = FileInputStream(verificationsOut) + val reader = BufferedReader(InputStreamReader(verificationsOutIn)) + val verificationList: MutableList = mutableListOf() + var line: String + while (reader.readLine().also { line = it } != null) { + verificationList.add(fromString(line.trim())) + } + + return verificationList + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + override fun notBefore(timestamp: Date): InlineVerify = apply { + commandList.add("--not-before=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun notAfter(timestamp: Date): InlineVerify = apply { + commandList.add("--not-after=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun cert(cert: InputStream): InlineVerify = apply { + commandList.add("@ENV:CERT_$argCounter") + envList.add("CERT_$argCounter=${ExternalSOP.readString(cert)}") + argCounter += 1 + } +} From f2204dfd4d091d868e9435e672187ced4c58b56e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 18:06:35 +0100 Subject: [PATCH 152/298] Kotlin conversion: ListProfilesExternal --- .../operation/ListProfilesExternal.java | 50 ------------------- .../operation/ListProfilesExternal.kt | 36 +++++++++++++ 2 files changed, 36 insertions(+), 50 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java b/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java deleted file mode 100644 index 21d5e13..0000000 --- a/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Profile; -import sop.external.ExternalSOP; -import sop.operation.ListProfiles; - -import javax.annotation.Nonnull; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -public class ListProfilesExternal implements ListProfiles { - - private final List commandList = new ArrayList<>(); - private final List envList; - - public ListProfilesExternal(String binary, Properties properties) { - this.commandList.add(binary); - this.commandList.add("list-profiles"); - this.envList = ExternalSOP.propertiesToEnv(properties); - } - - @Override - @Nonnull - public List subcommand(@Nonnull String command) { - commandList.add(command); - try { - String output = new String(ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList).getBytes()); - return toProfiles(output); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static List toProfiles(String output) { - List profiles = new ArrayList<>(); - for (String line : output.split("\n")) { - if (line.trim().isEmpty()) { - continue; - } - profiles.add(Profile.parse(line)); - } - return profiles; - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt new file mode 100644 index 0000000..5e8ff89 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.IOException +import java.util.Properties +import sop.Profile +import sop.external.ExternalSOP +import sop.operation.ListProfiles + +/** Implementation of the [ListProfiles] operation using an external SOP binary. */ +class ListProfilesExternal(binary: String, environment: Properties) : ListProfiles { + + private val commandList = mutableListOf(binary, "list-profiles") + private val envList = ExternalSOP.propertiesToEnv(environment) + + override fun subcommand(command: String): List { + return try { + String( + ExternalSOP.executeProducingOperation( + Runtime.getRuntime(), commandList.plus(command), envList) + .bytes) + .let { toProfiles(it) } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + companion object { + @JvmStatic + private fun toProfiles(output: String): List = + output.split("\n").filter { it.isNotBlank() }.map { Profile.parse(it) } + } +} From 832a455c4ccb885d68ebb97e6b855d0e80faa24d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 18:11:24 +0100 Subject: [PATCH 153/298] Kotlin conversion: RevokeKeyExternal --- .../external/operation/RevokeKeyExternal.java | 52 ------------------- .../external/operation/RevokeKeyExternal.kt | 31 +++++++++++ 2 files changed, 31 insertions(+), 52 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/RevokeKeyExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java b/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java deleted file mode 100644 index 72ed549..0000000 --- a/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Ready; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.RevokeKey; - -import javax.annotation.Nonnull; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -public class RevokeKeyExternal implements RevokeKey { - - private final List commandList = new ArrayList<>(); - private final List envList; - - private int withKeyPasswordCounter = 0; - - public RevokeKeyExternal(String binary, Properties environment) { - this.commandList.add(binary); - this.commandList.add("revoke-key"); - this.envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public RevokeKey noArmor() { - this.commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public RevokeKey withKeyPassword(@Nonnull byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { - String envVar = "KEY_PASSWORD_" + withKeyPasswordCounter++; - commandList.add("--with-key-password=@ENV:" + envVar); - envList.add(envVar + "=" + new String(password)); - return this; - } - - @Override - @Nonnull - public Ready keys(@Nonnull InputStream keys) { - return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys); - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/RevokeKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/RevokeKeyExternal.kt new file mode 100644 index 0000000..43795e6 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/RevokeKeyExternal.kt @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.RevokeKey + +/** Implementation of the [RevokeKey] operation using an external SOP binary. */ +class RevokeKeyExternal(binary: String, environment: Properties) : RevokeKey { + + private val commandList = mutableListOf(binary, "revoke-key") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCount = 0 + + override fun noArmor(): RevokeKey = apply { commandList.add("--no-armor") } + + override fun withKeyPassword(password: ByteArray): RevokeKey = apply { + commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount") + envList.add("KEY_PASSWORD_$argCount=${String(password)}") + argCount += 1 + } + + override fun keys(keys: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys) +} From 3eaae149b721f046ae22b4835782f8d49a0d8efe Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 18:34:30 +0100 Subject: [PATCH 154/298] Kotlin conversion: VersionExternal --- .../external/operation/VersionExternal.java | 163 ------------------ .../sop/external/operation/package-info.java | 8 - .../main/java/sop/external/package-info.java | 8 - .../sop/external/operation/VersionExternal.kt | 98 +++++++++++ 4 files changed, 98 insertions(+), 179 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/VersionExternal.java delete mode 100644 external-sop/src/main/java/sop/external/operation/package-info.java delete mode 100644 external-sop/src/main/java/sop/external/package-info.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/VersionExternal.java b/external-sop/src/main/java/sop/external/operation/VersionExternal.java deleted file mode 100644 index ab2bbef..0000000 --- a/external-sop/src/main/java/sop/external/operation/VersionExternal.java +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.external.ExternalSOP; -import sop.operation.Version; - -import javax.annotation.Nonnull; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.Properties; - -/** - * Implementation of the {@link Version} operation using an external SOP binary. - */ -public class VersionExternal implements Version { - - private final Runtime runtime = Runtime.getRuntime(); - private final String binary; - private final Properties environment; - - public VersionExternal(String binaryName, Properties environment) { - this.binary = binaryName; - this.environment = environment; - } - - @Override - @Nonnull - public String getName() { - String[] command = new String[] {binary, "version"}; - String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); - try { - Process process = runtime.exec(command, env); - BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); - String line = stdInput.readLine().trim(); - ExternalSOP.finish(process); - if (line.contains(" ")) { - return line.substring(0, line.lastIndexOf(" ")); - } - return line; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - @Nonnull - public String getVersion() { - String[] command = new String[] {binary, "version"}; - String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); - try { - Process process = runtime.exec(command, env); - BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); - String line = stdInput.readLine().trim(); - ExternalSOP.finish(process); - if (line.contains(" ")) { - return line.substring(line.lastIndexOf(" ") + 1); - } - return line; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - @Nonnull - public String getBackendVersion() { - String[] command = new String[] {binary, "version", "--backend"}; - String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); - try { - Process process = runtime.exec(command, env); - BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = stdInput.readLine()) != null) { - sb.append(line).append('\n'); - } - ExternalSOP.finish(process); - return sb.toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - @Nonnull - public String getExtendedVersion() { - String[] command = new String[] {binary, "version", "--extended"}; - String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); - try { - Process process = runtime.exec(command, env); - BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = stdInput.readLine()) != null) { - sb.append(line).append('\n'); - } - ExternalSOP.finish(process); - return sb.toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public int getSopSpecRevisionNumber() { - String revision = getSopSpecVersion(); - String firstLine; - if (revision.contains("\n")) { - firstLine = revision.substring(0, revision.indexOf("\n")); - } else { - firstLine = revision; - } - - if (!firstLine.contains("-")) { - return -1; - } - - return Integer.parseInt(firstLine.substring(firstLine.lastIndexOf("-") + 1)); - } - - @Override - public boolean isSopSpecImplementationIncomplete() { - String revision = getSopSpecVersion(); - return revision.startsWith("~"); - } - - @Override - public String getSopSpecImplementationRemarks() { - String revision = getSopSpecVersion(); - if (revision.contains("\n")) { - String tail = revision.substring(revision.indexOf("\n") + 1).trim(); - - if (!tail.isEmpty()) { - return tail; - } - } - return null; - } - - @Override - @Nonnull - public String getSopSpecVersion() { - String[] command = new String[] {binary, "version", "--sop-spec"}; - String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); - try { - Process process = runtime.exec(command, env); - BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = stdInput.readLine()) != null) { - sb.append(line).append('\n'); - } - ExternalSOP.finish(process); - return sb.toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/external-sop/src/main/java/sop/external/operation/package-info.java b/external-sop/src/main/java/sop/external/operation/package-info.java deleted file mode 100644 index 9c7dd29..0000000 --- a/external-sop/src/main/java/sop/external/operation/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Bindings for SOP subcommands to a SOP binary. - */ -package sop.external.operation; diff --git a/external-sop/src/main/java/sop/external/package-info.java b/external-sop/src/main/java/sop/external/package-info.java deleted file mode 100644 index 208985e..0000000 --- a/external-sop/src/main/java/sop/external/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Implementation of sop-java which delegates execution to a binary implementing the SOP command line interface. - */ -package sop.external; diff --git a/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt new file mode 100644 index 0000000..7e13fc1 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.IOException +import java.util.Properties +import sop.external.ExternalSOP +import sop.operation.Version + +/** Implementation of the [Version] operation using an external SOP binary. */ +class VersionExternal(binary: String, environment: Properties) : Version { + + private val commandList = listOf(binary, "version") + private val envList = ExternalSOP.propertiesToEnv(environment) + + override fun getName(): String { + val info = executeForLine(commandList) + return if (info.contains(" ")) { + info.substring(0, info.lastIndexOf(" ")) + } else { + info + } + } + + override fun getVersion(): String { + val info = executeForLine(commandList) + return if (info.contains(" ")) { + info.substring(info.lastIndexOf(" ") + 1) + } else { + info + } + } + + override fun getBackendVersion(): String { + return executeForLines(commandList.plus("--backend")) + } + + override fun getExtendedVersion(): String { + return executeForLines(commandList.plus("--extended")) + } + + override fun getSopSpecRevisionNumber(): Int { + val revision = getSopSpecVersion() + val firstLine = + if (revision.contains("\n")) { + revision.substring(0, revision.indexOf("\n")) + } else { + revision + } + + if (!firstLine.contains("-")) { + return -1 + } + return Integer.parseInt(firstLine.substring(firstLine.lastIndexOf("-") + 1)) + } + + override fun isSopSpecImplementationIncomplete(): Boolean { + return getSopSpecVersion().startsWith("~") + } + + override fun getSopSpecImplementationRemarks(): String? { + val revision = getSopSpecVersion() + if (revision.contains("\n")) { + revision.substring(revision.indexOf("\n")).trim().takeIf { it.isNotBlank() } + } + return null + } + + override fun getSopSpecVersion(): String { + return executeForLines(commandList.plus("--sop-spec")) + } + + private fun executeForLine(commandList: List): String { + return try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val result = process.inputStream.bufferedReader().readLine() + ExternalSOP.finish(process) + result.trim() + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + private fun executeForLines(commandList: List): String { + return try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val result = process.inputStream.bufferedReader().readLines().joinToString("\n") + ExternalSOP.finish(process) + result.trim() + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 6c952efca23ae932e9d8e6e691aa555f50953eb4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 18:50:34 +0100 Subject: [PATCH 155/298] Fix NPE in line iterator --- .../kotlin/sop/external/operation/InlineVerifyExternal.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt index ed769e3..bf0c66b 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt @@ -62,9 +62,9 @@ class InlineVerifyExternal( val verificationsOutIn = FileInputStream(verificationsOut) val reader = BufferedReader(InputStreamReader(verificationsOutIn)) val verificationList: MutableList = mutableListOf() - var line: String + var line: String? while (reader.readLine().also { line = it } != null) { - verificationList.add(fromString(line.trim())) + verificationList.add(fromString(line!!.trim())) } return verificationList From 60758dfa2f71817530a4371a0c244456095d3b27 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 18:58:14 +0100 Subject: [PATCH 156/298] Update changelog --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d736ded..f168283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,12 @@ SPDX-License-Identifier: Apache-2.0 # Changelog ## 8.0.0-SNAPSHOT -- Rewrote API in Kotlin +- Rewrote `sop-java` in Kotlin +- Rewrote `sop-java-picocli` in Kotlin +- Rewrote `external-sop` in Kotlin - Update implementation to [SOP Specification revision 08](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html). - Add `--no-armor` option to `revoke-key` and `change-key-password` subcommands - - `armor`: Deprecate `--label` option + - `armor`: Deprecate `--label` option in `sop-java` and remove in `sop-java-picocli` - `encrypt`: Add `--session-key-out` option - Slight API changes: - `sop.encrypt().plaintext()` now returns a `ReadyWithResult` instead of `Ready`. From 7eeb159f1248347bf2cdff36322cf03f7ec93894 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 18:59:50 +0100 Subject: [PATCH 157/298] SOP-Java 8.0.0 --- CHANGELOG.md | 2 +- version.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f168283..4b5d562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 8.0.0-SNAPSHOT +## 8.0.0 - Rewrote `sop-java` in Kotlin - Rewrote `sop-java-picocli` in Kotlin - Rewrote `external-sop` in Kotlin diff --git a/version.gradle b/version.gradle index c5a9cb1..a7c71aa 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '8.0.0' - isSnapshot = true + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From ae83ddcff6adc598b3f17a7d2bcbf34279fc8619 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 19:01:50 +0100 Subject: [PATCH 158/298] SOP-Java 8.0.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index a7c71aa..b1d7770 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '8.0.0' - isSnapshot = false + shortVersion = '8.0.1' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 51d9c298379faebad4c63655ecfe9071b163a753 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 22 Nov 2023 17:11:24 +0100 Subject: [PATCH 159/298] decrypt --verify-with: Do not expect exit 3 when verifications is empty --- .../sop/cli/picocli/commands/DecryptCmdTest.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 c2c67ba..62070c2 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 @@ -21,6 +21,7 @@ import sop.operation.Decrypt; import sop.util.HexUtil; import sop.util.UTCUtil; +import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; @@ -247,15 +248,17 @@ public class DecryptCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.NoSignature.EXIT_CODE) - public void assertNoSignatureExceptionCausesExit3() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { + public void assertNoVerificationsIsOkay() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { + File tempFile = File.createTempFile("verify-with-", ".tmp"); + File verifyOut = new File(tempFile.getParent(), "verifications.out"); + verifyOut.deleteOnExit(); when(decrypt.ciphertext((InputStream) any())).thenReturn(new ReadyWithResult() { @Override - public DecryptionResult writeTo(OutputStream outputStream) throws SOPGPException.NoSignature { - throw new SOPGPException.NoSignature(); + public DecryptionResult writeTo(@Nonnull OutputStream outputStream) throws SOPGPException.NoSignature { + return new DecryptionResult(null, Collections.emptyList()); } }); - SopCLI.main(new String[] {"decrypt"}); + SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath(), "--verifications-out", verifyOut.getAbsolutePath()}); } @Test From e5e64003f3840726cbf92021e0019757a4ecfaff Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 22 Nov 2023 17:11:48 +0100 Subject: [PATCH 160/298] decrypt: Do not throw NoSignature exception when verifications is empty --- .../kotlin/sop/cli/picocli/commands/DecryptCmd.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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 1e688b3..3f15f07 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 @@ -85,13 +85,10 @@ class DecryptCmd : AbstractSopCmd() { @Throws(IOException::class) private fun writeVerifyOut(result: DecryptionResult) { verifyOut?.let { - if (result.verifications.isEmpty()) { - val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found") - throw NoSignature(errorMsg) - } - - getOutput(verifyOut).use { out -> - PrintWriter(out).use { pw -> result.verifications.forEach { pw.println(it) } } + getOutput(it).use { out -> + PrintWriter(out).use { pw -> + result.verifications.forEach { verification -> pw.println(verification) } + } } } } From 592aecd64605deabd7cb7b9d27167b35e79f6a31 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 22 Nov 2023 18:19:13 +0100 Subject: [PATCH 161/298] SOP-Java 8.0.1 --- CHANGELOG.md | 3 +++ version.gradle | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b5d562..f188b0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 8.0.1 +- `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty. + ## 8.0.0 - Rewrote `sop-java` in Kotlin - Rewrote `sop-java-picocli` in Kotlin diff --git a/version.gradle b/version.gradle index b1d7770..74bcad6 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '8.0.1' - isSnapshot = true + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 7092baee4f664941c27304312b653899d5cd27da Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 22 Nov 2023 18:21:41 +0100 Subject: [PATCH 162/298] SOP-Java 8.0.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 74bcad6..434fdc5 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '8.0.1' - isSnapshot = false + shortVersion = '8.0.2' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From e2a568e73e490d2c96f0f84e99b348b40e456fcf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 26 Feb 2024 11:03:16 +0100 Subject: [PATCH 163/298] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..4f92d0e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** + + +**Version** + +- `sop-java`: +- `pgpainless-core`: +- `bouncycastle`: + +**To Reproduce** + +``` +Example Code Block +``` + +**Expected behavior** + + +**Additional context** + From d5d7d67d6f400430d89693ca4066d60742e434ab Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 17:58:04 +0100 Subject: [PATCH 164/298] Fix reuse compliance --- .reuse/dep5 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.reuse/dep5 b/.reuse/dep5 index 8feb5a2..f43556c 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -22,3 +22,8 @@ License: Apache-2.0 Files: external-sop/src/main/resources/sop/testsuite/external/* Copyright: 2023 the original author or authors License: Apache-2.0 + +# Github Issue Templates +Files: .github/ISSUE_TEMPLATE/* +Copyright: 2024 the original author or authors +License: Apache-2.0 From 03f8950b16c72aa5b5ab771a6b21e8e0e94f19eb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 18:05:35 +0100 Subject: [PATCH 165/298] Rename woodpecker files --- .woodpecker/{.build.yml => build.yml} | 0 .woodpecker/{.reuse.yml => reuse.yml} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename .woodpecker/{.build.yml => build.yml} (100%) rename .woodpecker/{.reuse.yml => reuse.yml} (100%) diff --git a/.woodpecker/.build.yml b/.woodpecker/build.yml similarity index 100% rename from .woodpecker/.build.yml rename to .woodpecker/build.yml diff --git a/.woodpecker/.reuse.yml b/.woodpecker/reuse.yml similarity index 100% rename from .woodpecker/.reuse.yml rename to .woodpecker/reuse.yml From 173bc55eb9a6110894d845e5eba8ef9ce5784c3e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:41:44 +0100 Subject: [PATCH 166/298] 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 167/298] 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 168/298] 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 169/298] 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 170/298] 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 1df57475493a18df3303f0162ec5c98259d53387 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:37:46 +0200 Subject: [PATCH 171/298] 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 172/298] 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 173/298] 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 174/298] 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 175/298] 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 176/298] 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 177/298] 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 178/298] 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 179/298] 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 180/298] 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 181/298] 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 182/298] 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 183/298] 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 184/298] 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 185/298] 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 186/298] 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 187/298] 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 188/298] 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 189/298] 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 190/298] 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 191/298] 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 192/298] 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 193/298] 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 194/298] 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 195/298] 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 196/298] 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 197/298] 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 198/298] 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 199/298] 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 200/298] 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 201/298] 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 202/298] 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 203/298] 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 204/298] 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 205/298] 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 206/298] 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 207/298] 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 208/298] 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 209/298] 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 210/298] 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 211/298] 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 212/298] 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 213/298] 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 214/298] 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 215/298] 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 216/298] 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 217/298] 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 218/298] 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 2c26ab2da524cbf28f705262e5b4f9952becff7f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:01:20 +0200 Subject: [PATCH 219/298] Improve reproducibility --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 78a7267..4cc8dc4 100644 --- a/build.gradle +++ b/build.gradle @@ -78,6 +78,9 @@ allprojects { tasks.withType(AbstractArchiveTask) { preserveFileTimestamps = false reproducibleFileOrder = true + + dirMode = 0755 + fileMode = 0644 } // Compatibility of default implementations in kotlin interfaces with Java implementations. From 8394f2e5a8e40df8461a4845e09e921e0ab81c79 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:01:45 +0200 Subject: [PATCH 220/298] Make use of toolchain functionality and raise min Java API level to 11 --- build.gradle | 22 ++++++++++------------ version.gradle | 2 +- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/build.gradle b/build.gradle index 4cc8dc4..c73b60c 100644 --- a/build.gradle +++ b/build.gradle @@ -68,8 +68,6 @@ allprojects { description = "Stateless OpenPGP Protocol API for Java" version = shortVersion - sourceCompatibility = javaSourceCompatibility - repositories { mavenCentral() } @@ -83,6 +81,10 @@ allprojects { fileMode = 0644 } + kotlin { + jvmToolchain(javaSourceCompatibility) + } + // Compatibility of default implementations in kotlin interfaces with Java implementations. tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions { @@ -115,7 +117,7 @@ allprojects { } jacoco { - toolVersion = "0.8.7" + toolVersion = "0.8.8" } jacocoTestReport { @@ -123,7 +125,7 @@ allprojects { sourceDirectories.setFrom(project.files(sourceSets.main.allSource.srcDirs)) classDirectories.setFrom(project.files(sourceSets.main.output)) reports { - xml.enabled true + xml.required = true } } @@ -141,15 +143,15 @@ subprojects { apply plugin: 'signing' task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' + archiveClassifier = 'sources' from sourceSets.main.allSource } task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' + archiveClassifier = 'javadoc' from javadoc.destinationDir } task testsJar(type: Jar, dependsOn: testClasses) { - classifier = 'tests' + archiveClassifier = 'tests' from sourceSets.test.output } @@ -246,7 +248,7 @@ task jacocoRootReport(type: JacocoReport) { classDirectories.setFrom(files(subprojects.sourceSets.main.output)) executionData.setFrom(files(subprojects.jacocoTestReport.executionData)) reports { - xml.enabled true + xml.required = true xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml") } // We could remove the following setOnlyIf line, but then @@ -257,10 +259,6 @@ task jacocoRootReport(type: JacocoReport) { } task javadocAll(type: Javadoc) { - def currentJavaVersion = JavaVersion.current() - if (currentJavaVersion.compareTo(JavaVersion.VERSION_1_9) >= 0) { - options.addStringOption("-release", "8"); - } source subprojects.collect {project -> project.sourceSets.main.allJava } destinationDir = new File(buildDir, 'javadoc') diff --git a/version.gradle b/version.gradle index 33a2251..9619f9a 100644 --- a/version.gradle +++ b/version.gradle @@ -7,7 +7,7 @@ allprojects { shortVersion = '10.1.1' isSnapshot = true minAndroidSdk = 10 - javaSourceCompatibility = 1.8 + javaSourceCompatibility = 11 gsonVersion = '2.10.1' jsrVersion = '3.0.2' junitVersion = '5.8.2' From cddc92bd92ef5354c10a5236fe8ba75e890c5096 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:13:40 +0200 Subject: [PATCH 221/298] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3028216..1746078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ SPDX-License-Identifier: Apache-2.0 ## 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. +- gradle: Make use of jvmToolchain functionality +- gradle: Improve reproducibility ## 10.1.0 - `sop-java`: From a2a3bda2b3c321c1561d217cb2f5cae7f5f6a5c8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:26:09 +0200 Subject: [PATCH 222/298] Migrate AbortOnUnsupportedOption annotation back to java --- .../testsuite/AbortOnUnsupportedOption.java | 18 +++++++++++++ .../AbortOnUnsupportedOptionExtension.java | 26 +++++++++++++++++++ .../sop/testsuite/AbortOnUnsupportedOption.kt | 12 --------- .../AbortOnUnsupportedOptionExtension.kt | 21 --------------- 4 files changed, 44 insertions(+), 33 deletions(-) create mode 100644 sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOption.java create mode 100644 sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOptionExtension.java delete mode 100644 sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt delete mode 100644 sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOption.java b/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOption.java new file mode 100644 index 0000000..cbd0746 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOption.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface AbortOnUnsupportedOption { + +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOptionExtension.java b/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOptionExtension.java new file mode 100644 index 0000000..0bf366d --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOptionExtension.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; +import sop.exception.SOPGPException; + +import java.lang.annotation.Annotation; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +public class AbortOnUnsupportedOptionExtension implements TestExecutionExceptionHandler { + + @Override + public void handleTestExecutionException(ExtensionContext extensionContext, Throwable throwable) throws Throwable { + Class testClass = extensionContext.getRequiredTestClass(); + Annotation annotation = testClass.getAnnotation(AbortOnUnsupportedOption.class); + if (annotation != null && throwable instanceof SOPGPException.UnsupportedOption) { + assumeTrue(false, "Test aborted due to: " + throwable.getMessage()); + } + throw throwable; + } +} diff --git a/sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt b/sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt deleted file mode 100644 index cf99671..0000000 --- a/sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt +++ /dev/null @@ -1,12 +0,0 @@ -// 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-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt b/sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt deleted file mode 100644 index 809c78f..0000000 --- a/sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt +++ /dev/null @@ -1,21 +0,0 @@ -// 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 e3fe9410d7dbd30c08e86810758fb47a20c71970 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:27:54 +0200 Subject: [PATCH 223/298] reuse: Migrate to toml format --- .reuse/dep5 | 29 ----------------------------- REUSE.toml | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 29 deletions(-) delete mode 100644 .reuse/dep5 create mode 100644 REUSE.toml diff --git a/.reuse/dep5 b/.reuse/dep5 deleted file mode 100644 index f43556c..0000000 --- a/.reuse/dep5 +++ /dev/null @@ -1,29 +0,0 @@ -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: SOP-Java -Upstream-Contact: Paul Schaub -Source: https://pgpainless.org - -# Sample paragraph, commented out: -# -# Files: src/* -# Copyright: $YEAR $NAME <$CONTACT> -# License: ... - -# Gradle build tool -Files: gradle* -Copyright: 2015 the original author or authors. -License: Apache-2.0 - -# Woodpecker build files -Files: .woodpecker/* -Copyright: 2022 the original author or authors. -License: Apache-2.0 - -Files: external-sop/src/main/resources/sop/testsuite/external/* -Copyright: 2023 the original author or authors -License: Apache-2.0 - -# Github Issue Templates -Files: .github/ISSUE_TEMPLATE/* -Copyright: 2024 the original author or authors -License: Apache-2.0 diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..f82d8f8 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: 2025 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 + +version = 1 +SPDX-PackageName = "SOP-Java" +SPDX-PackageSupplier = "Paul Schaub " +SPDX-PackageDownloadLocation = "https://pgpainless.org" + +[[annotations]] +path = "gradle**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2015 the original author or authors." +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ".woodpecker/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 the original author or authors." +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "external-sop/src/main/resources/sop/testsuite/external/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2023 the original author or authors" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ".github/ISSUE_TEMPLATE/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2024 the original author or authors" +SPDX-License-Identifier = "Apache-2.0" From 4d2876a296539ec1e867d33c7228e105c9f390e0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:28:14 +0200 Subject: [PATCH 224/298] Fix formatting issue --- sop-java/src/main/kotlin/sop/SigningResult.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/SigningResult.kt b/sop-java/src/main/kotlin/sop/SigningResult.kt index 60888e0..651f8c1 100644 --- a/sop-java/src/main/kotlin/sop/SigningResult.kt +++ b/sop-java/src/main/kotlin/sop/SigningResult.kt @@ -9,9 +9,10 @@ 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](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. + * 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) { From 2d99aea4ab7ec2fa1e32f309fc21f7f7fcf0e492 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:29:04 +0200 Subject: [PATCH 225/298] Bump animalsniffer to 2.0.0 --- CHANGELOG.md | 1 + build.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1746078..b2f0a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ SPDX-License-Identifier: Apache-2.0 configuration files for reflection, resources and dynamic proxies. - gradle: Make use of jvmToolchain functionality - gradle: Improve reproducibility +- gradle: Bump animalsniffer to `2.0.0` ## 10.1.0 - `sop-java`: diff --git a/build.gradle b/build.gradle index c73b60c..1bff704 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { } plugins { - id 'ru.vyarus.animalsniffer' version '1.5.3' + id 'ru.vyarus.animalsniffer' version '2.0.0' id 'org.jetbrains.kotlin.jvm' version "1.9.21" id 'com.diffplug.spotless' version '6.22.0' apply false } From 701f9453ca0bb4125a07a91d3b6313a9f5d6df80 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:41:50 +0200 Subject: [PATCH 226/298] SOP-Java 10.1.1 --- CHANGELOG.md | 2 +- version.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2f0a18..c0500cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 10.1.1-SNAPSHOT +## 10.1.1 - Prepare jar files for use in native images, e.g. using GraalVM by generating and including configuration files for reflection, resources and dynamic proxies. - gradle: Make use of jvmToolchain functionality diff --git a/version.gradle b/version.gradle index 9619f9a..5fdbcad 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '10.1.1' - isSnapshot = true + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 11 gsonVersion = '2.10.1' From cbeec9c90d68ff7ebdcb8ca146ae4481ba2b787f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:44:22 +0200 Subject: [PATCH 227/298] SOP-Java 10.1.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 5fdbcad..b4908ee 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '10.1.1' - isSnapshot = false + shortVersion = '10.1.2' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 11 gsonVersion = '2.10.1' From ad137d63514822945cb94a0487f36b0dc5eb91b8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 May 2025 02:16:52 +0200 Subject: [PATCH 228/298] Try to fix coveralls repo token --- .woodpecker/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index 2138cb4..4c23ffb 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -17,5 +17,5 @@ steps: # Code has coverage - gradle jacocoRootReport coveralls environment: - coveralls_repo_token: + COVERALLS_REPO_TOKEN: from_secret: coveralls_repo_token From 1dcf13244d1a0e4ac668f01a1d10dc6f3f4294ec Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Sep 2024 22:40:36 +0200 Subject: [PATCH 229/298] 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 4115a5041d0c49242f61231e37c283863e862df0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Sep 2024 22:43:36 +0200 Subject: [PATCH 230/298] 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 84404d629fa91a5d70399a9043486093ee7632c2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Sep 2024 22:43:50 +0200 Subject: [PATCH 231/298] 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 ada77be955720dfca424c27b763878e453b2b65b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 15:50:17 +0200 Subject: [PATCH 232/298] 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 d6c133087435fa78f0ae246bbf8f4d8c2a388295 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 16:01:30 +0200 Subject: [PATCH 233/298] 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 2b6a5dd651d0063be1558e1de7ae357a83eff0af Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 16:01:58 +0200 Subject: [PATCH 234/298] 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 122cd016a1330bf6b723e84fc829da990d8de36f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 16:56:26 +0200 Subject: [PATCH 235/298] 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 4ed326a14297e2dcae58e1ad9e13dcff70ff064d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 16:56:25 +0200 Subject: [PATCH 236/298] 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 dd12e289263557357be7287edc9400361ee61e75 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 16:56:43 +0200 Subject: [PATCH 237/298] 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 69fbfc09a7a77040dc0b7004e3674ec07cc6d4ed Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 17:51:04 +0200 Subject: [PATCH 238/298] 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 0bb50952c5baa48d237990bd739102b192d280e8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 18:25:03 +0200 Subject: [PATCH 239/298] 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 40ccb8cc9936f8d32252e67fcbfe7e853c3ad9da Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 18:25:17 +0200 Subject: [PATCH 240/298] 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 f7cc9ab816f51ffa17780bd437a960443fbaddc2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 18:40:55 +0200 Subject: [PATCH 241/298] 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 f1bdce99cb4ccc14d76db6f2bd425f3766cb8388 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 19:19:56 +0200 Subject: [PATCH 242/298] 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 4cf410a9f9282fb142d922ac30efc32e66e29286 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:26:36 +0200 Subject: [PATCH 243/298] Bump version --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index b4908ee..c757e1e 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '10.1.2' + shortVersion = '11.0.0' isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 11 From 7f1c1b1aae0e27b8f0c5efdbbb436a1ae60b4ced Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:49:00 +0200 Subject: [PATCH 244/298] 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 5105b6f4addc139e21b1e6025927a4b3a98fa1a0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 8 Apr 2025 13:13:15 +0200 Subject: [PATCH 245/298] 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 From b300be42a4c2c3c1be0ee1e70dde9ceb3d1ffb72 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 8 Apr 2025 17:08:06 +0200 Subject: [PATCH 246/298] validate-userid: Add --validate-at option --- .../sop/external/operation/ValidateUserIdExternal.kt | 5 +++++ .../kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt | 7 +++++++ sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt | 3 +++ 3 files changed, 15 insertions(+) diff --git a/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt index 867a755..581d6f5 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt @@ -8,6 +8,7 @@ import java.io.InputStream import java.util.* import sop.external.ExternalSOP import sop.operation.ValidateUserId +import sop.util.UTCUtil class ValidateUserIdExternal(binary: String, environment: Properties) : ValidateUserId { @@ -35,4 +36,8 @@ class ValidateUserIdExternal(binary: String, environment: Properties) : Validate .bytes return true } + + override fun validateAt(date: Date): ValidateUserId = apply { + commandList.add("--validate-at=${UTCUtil.formatUTCDate(date)}") + } } 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 da81a27..9a09a15 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 @@ -11,6 +11,7 @@ import picocli.CommandLine.Parameters import sop.cli.picocli.SopCLI import sop.exception.SOPGPException import sop.util.HexUtil.Companion.bytesToHex +import java.util.* @Command( name = "validate-userid", @@ -21,6 +22,8 @@ class ValidateUserIdCmd : AbstractSopCmd() { @Option(names = ["--addr-spec-only"]) var addrSpecOnly: Boolean = false + @Option(names = ["--validate-at"]) var validateAt: Date? = null + @Parameters(index = "0", arity = "1", paramLabel = "USERID") lateinit var userId: String @Parameters(index = "1..*", arity = "1..*", paramLabel = "CERTS") @@ -34,6 +37,10 @@ class ValidateUserIdCmd : AbstractSopCmd() { validateUserId.addrSpecOnly() } + if (validateAt != null) { + validateUserId.validateAt(validateAt!!) + } + validateUserId.userId(userId) for (authority in authorities) { diff --git a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt index 4f4c51a..fb8cab6 100644 --- a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt +++ b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt @@ -7,6 +7,7 @@ package sop.operation import java.io.IOException import java.io.InputStream import sop.exception.SOPGPException +import java.util.* /** Subcommand to validate UserIDs on certificates. */ interface ValidateUserId { @@ -75,4 +76,6 @@ interface ValidateUserId { @Throws( SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) fun subjects(certs: ByteArray): Boolean = subjects(certs.inputStream()) + + fun validateAt(date: Date): ValidateUserId } From 082cbde869e72615eec2ca6ffe50570f545cabf4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 12:26:29 +0200 Subject: [PATCH 247/298] MergeCertsCmd: Fix default value of armor --- .../src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 16d56e3..3dcef38 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 @@ -16,7 +16,7 @@ import sop.exception.SOPGPException 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 = true @CommandLine.Parameters(paramLabel = "CERTS") var updates: List = listOf() From a72545e3b9d4f2cb2ca8be3c36b7a7a3ced49296 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 12:46:50 +0200 Subject: [PATCH 248/298] Fix formatting --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 8 ++++++-- .../kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt | 2 +- sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt | 2 +- 3 files changed, 8 insertions(+), 4 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 943f0f3..98701fc 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 @@ -88,7 +88,10 @@ class SopCLI { // 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 + 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 @@ -96,7 +99,8 @@ class SopCLI { executionExceptionHandler = SOPExecutionExceptionHandler() exitCodeExceptionMapper = SOPExceptionExitCodeMapper() isCaseInsensitiveEnumValuesAllowed = true - }.execute(*args) + } + .execute(*args) } } 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 9a09a15..b83e5a8 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 @@ -5,13 +5,13 @@ package sop.cli.picocli.commands import java.io.IOException +import java.util.* 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 -import java.util.* @Command( name = "validate-userid", diff --git a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt index fb8cab6..fe20fd4 100644 --- a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt +++ b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt @@ -6,8 +6,8 @@ package sop.operation import java.io.IOException import java.io.InputStream -import sop.exception.SOPGPException import java.util.* +import sop.exception.SOPGPException /** Subcommand to validate UserIDs on certificates. */ interface ValidateUserId { From 65aa0afd4ee760b60a8b6162551a54f0b5bd113d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 12:47:00 +0200 Subject: [PATCH 249/298] Add new Exception types --- .../kotlin/sop/exception/SOPGPException.kt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt index 862e1bd..9df1628 100644 --- a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt +++ b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt @@ -16,6 +16,22 @@ abstract class SOPGPException : RuntimeException { abstract fun getExitCode(): Int + /** An otherwise unspecified failure occurred */ + class UnspecificFailure : SOPGPException { + + constructor(message: String) : super(message) + + constructor(message: String, e: Throwable) : super(message, e) + + constructor(e: Throwable) : super(e) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 1 + } + } + /** No acceptable signatures found (sop verify, inline-verify). */ class NoSignature : SOPGPException { @JvmOverloads @@ -378,4 +394,23 @@ abstract class SOPGPException : RuntimeException { const val EXIT_CODE = 107 } } + + /** + * Key not certification-capable (e.g., expired, revoked, unacceptable usage flags) (sop + * certify-userid) + */ + class KeyCannotCertify : SOPGPException { + + constructor(message: String) : super(message) + + constructor(message: String, e: Throwable) : super(message, e) + + constructor(e: Throwable) : super(e) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 109 + } + } } From 68bab9cbb433ba0d5f1ee91bd3f6270e9781f6ea Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 13:50:35 +0200 Subject: [PATCH 250/298] reuse: convert dep5 file to toml file --- REUSE.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/REUSE.toml b/REUSE.toml index f82d8f8..7e1b250 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -1,6 +1,6 @@ -# SPDX-FileCopyrightText: 2025 Paul Schaub +# SPDX-FileCopyrightText: 2025 Paul Schaub # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: CC0-1.0 version = 1 SPDX-PackageName = "SOP-Java" From a8497617d58e5c70391715713d809a74e99fed5a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 13:50:57 +0200 Subject: [PATCH 251/298] Add basic test for certify-userid and validate-userid subcommands --- .../operation/CertifyValidateUserIdTest.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java new file mode 100644 index 0000000..eabda44 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +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 sop.SOP; +import sop.exception.SOPGPException; + +import java.io.IOException; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class CertifyValidateUserIdTest { + + static Stream provideInstances() { + return AbstractSOPTest.provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void certifyUserId(SOP sop) throws IOException { + byte[] aliceKey = sop.generateKey() + .withKeyPassword("sw0rdf1sh") + .userId("Alice ") + .generate() + .getBytes(); + byte[] aliceCert = sop.extractCert() + .key(aliceKey) + .getBytes(); + + byte[] bobKey = sop.generateKey() + .userId("Bob ") + .generate() + .getBytes(); + byte[] bobCert = sop.extractCert() + .key(bobKey) + .getBytes(); + + // Alice has her own user-id self-certified + assertTrue(sop.validateUserId() + .authorities(aliceCert) + .userId("Alice ") + .subjects(aliceCert)); + + // Alice has not yet certified Bobs user-id + assertFalse(sop.validateUserId() + .authorities(aliceCert) + .userId("Bob ") + .subjects(bobCert)); + + byte[] bobCertifiedByAlice = sop.certifyUserId() + .userId("Bob ") + .withKeyPassword("sw0rdf1sh") + .keys(aliceKey) + .certs(bobCert) + .getBytes(); + + assertTrue(sop.validateUserId() + .userId("Bob ") + .authorities(aliceCert) + .subjects(bobCertifiedByAlice)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void addPetName(SOP sop) throws IOException { + byte[] aliceKey = sop.generateKey() + .userId("Alice ") + .generate() + .getBytes(); + byte[] aliceCert = sop.extractCert() + .key(aliceKey) + .getBytes(); + + byte[] bobKey = sop.generateKey() + .userId("Bob ") + .generate() + .getBytes(); + byte[] bobCert = sop.extractCert() + .key(bobKey) + .getBytes(); + + assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> + sop.certifyUserId() + .userId("Bobby") + .keys(aliceKey) + .certs(bobCert) + .getBytes()); + + byte[] bobWithPetName = sop.certifyUserId() + .userId("Bobby") + .noRequireSelfSig() + .keys(aliceKey) + .certs(bobCert) + .getBytes(); + + assertTrue(sop.validateUserId() + .userId("Bobby") + .authorities(aliceCert) + .subjects(bobWithPetName)); + } +} From a8cfb8fbf468f324d39cf00a2f501ade96f54a83 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 14:17:38 +0200 Subject: [PATCH 252/298] Improve test --- .../operation/CertifyValidateUserIdTest.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java index eabda44..488db9a 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java @@ -49,13 +49,15 @@ public class CertifyValidateUserIdTest { assertTrue(sop.validateUserId() .authorities(aliceCert) .userId("Alice ") - .subjects(aliceCert)); + .subjects(aliceCert), + "Alice accepts her own self-certified user-id"); // Alice has not yet certified Bobs user-id assertFalse(sop.validateUserId() .authorities(aliceCert) .userId("Bob ") - .subjects(bobCert)); + .subjects(bobCert), + "Alice has not yet certified Bobs user-id"); byte[] bobCertifiedByAlice = sop.certifyUserId() .userId("Bob ") @@ -67,7 +69,8 @@ public class CertifyValidateUserIdTest { assertTrue(sop.validateUserId() .userId("Bob ") .authorities(aliceCert) - .subjects(bobCertifiedByAlice)); + .subjects(bobCertifiedByAlice), + "Alice accepts Bobs user-id after she certified it"); } @ParameterizedTest @@ -94,7 +97,8 @@ public class CertifyValidateUserIdTest { .userId("Bobby") .keys(aliceKey) .certs(bobCert) - .getBytes()); + .getBytes(), + "Alice cannot create a pet-name for Bob without the --no-require-self-sig flag"); byte[] bobWithPetName = sop.certifyUserId() .userId("Bobby") @@ -106,6 +110,13 @@ public class CertifyValidateUserIdTest { assertTrue(sop.validateUserId() .userId("Bobby") .authorities(aliceCert) - .subjects(bobWithPetName)); + .subjects(bobWithPetName), + "Alice accepts the pet-name she gave to Bob"); + + assertFalse(sop.validateUserId() + .userId("Bobby") + .authorities(bobWithPetName) + .subjects(bobWithPetName), + "Bob does not accept the pet-name Alice gave him"); } } From 8c077a9c131554671f3e56575e0432c297753beb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 14:36:28 +0200 Subject: [PATCH 253/298] SOP update-key: Rename --no-new-mechanisms option to --no-added-capabilities --- .../main/kotlin/sop/external/operation/UpdateKeyExternal.kt | 2 +- .../main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt | 6 +++--- sop-java/src/main/kotlin/sop/operation/UpdateKey.kt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt index 9aa1d29..99df15e 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt @@ -21,7 +21,7 @@ class UpdateKeyExternal(binary: String, environment: Properties) : UpdateKey { override fun signingOnly(): UpdateKey = apply { commandList.add("--signing-only") } - override fun noNewMechanisms(): UpdateKey = apply { commandList.add("--no-new-mechanisms") } + override fun noAddedCapabilities(): UpdateKey = apply { commandList.add("--no-added-capabilities") } override fun withKeyPassword(password: ByteArray): UpdateKey = apply { commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount") 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 08f9297..931f241 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 @@ -20,7 +20,7 @@ class UpdateKeyCmd : AbstractSopCmd() { @Option(names = ["--signing-only"]) var signingOnly = false - @Option(names = ["--no-new-mechanisms"]) var noNewMechanisms = false + @Option(names = ["--no-added-capabilities"]) var noAddedCapabilities = false @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") var withKeyPassword: List = listOf() @@ -38,8 +38,8 @@ class UpdateKeyCmd : AbstractSopCmd() { updateKey.signingOnly() } - if (noNewMechanisms) { - updateKey.noNewMechanisms() + if (noAddedCapabilities) { + updateKey.noAddedCapabilities() } for (passwordFileName in withKeyPassword) { diff --git a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt index 6c32b22..f0a7c52 100644 --- a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt @@ -21,7 +21,7 @@ interface UpdateKey { @Throws(SOPGPException.UnsupportedOption::class) fun signingOnly(): UpdateKey - @Throws(SOPGPException.UnsupportedOption::class) fun noNewMechanisms(): UpdateKey + @Throws(SOPGPException.UnsupportedOption::class) fun noAddedCapabilities(): UpdateKey @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) fun withKeyPassword(password: String): UpdateKey = From dea7e905a977154d3a563223cfbcfafd96f551eb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 14:36:49 +0200 Subject: [PATCH 254/298] Document update key --- .../main/kotlin/sop/operation/UpdateKey.kt | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt index f0a7c52..9a31310 100644 --- a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt @@ -19,25 +19,61 @@ interface UpdateKey { */ fun noArmor(): UpdateKey + /** + * Allow key to be used for signing only. + * If this option is not present, the operation may add a new, encryption-capable component key. + */ @Throws(SOPGPException.UnsupportedOption::class) fun signingOnly(): UpdateKey + /** + * Do not allow adding new capabilities to the key. + * If this option is not present, the operation may add support for new capabilities to the key. + */ @Throws(SOPGPException.UnsupportedOption::class) fun noAddedCapabilities(): UpdateKey + /** + * Provide a passphrase for unlocking the secret key. + * + * @param password password + */ @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) fun withKeyPassword(password: String): UpdateKey = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + /** + * Provide a passphrase for unlocking the secret key. + * + * @param password password + */ @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) fun withKeyPassword(password: ByteArray): UpdateKey + /** + * Provide certificates that might contain updated signatures or third-party certifications. + * These certificates will be merged into the key. + * + * @param certs input stream of certificates + */ @Throws( SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) fun mergeCerts(certs: InputStream): UpdateKey + /** + * Provide certificates that might contain updated signatures or third-party certifications. + * These certificates will be merged into the key. + * + * @param certs binary certificates + */ @Throws( SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) fun mergeCerts(certs: ByteArray): UpdateKey = mergeCerts(certs.inputStream()) + /** + * Provide the OpenPGP key to update. + * + * @param key input stream containing the key + * @return handle to acquire the updated OpenPGP key from + */ @Throws( SOPGPException.BadData::class, IOException::class, @@ -45,6 +81,12 @@ interface UpdateKey { SOPGPException.PrimaryKeyBad::class) fun key(key: InputStream): Ready + /** + * Provide the OpenPGP key to update. + * + * @param key binary OpenPGP key + * @return handle to acquire the updated OpenPGP key from + */ @Throws( SOPGPException.BadData::class, IOException::class, From 091b5f9a5e19e1605e2981f440958d7f4952ef65 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 15:26:00 +0200 Subject: [PATCH 255/298] Add test for certifying with revoked key --- .../operation/CertifyValidateUserIdTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java index 488db9a..e05923c 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java @@ -119,4 +119,35 @@ public class CertifyValidateUserIdTest { .subjects(bobWithPetName), "Bob does not accept the pet-name Alice gave him"); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void certifyWithRevokedKey(SOP sop) throws IOException { + byte[] aliceKey = sop.generateKey() + .userId("Alice ") + .generate() + .getBytes(); + byte[] aliceRevokedCert = sop.revokeKey() + .keys(aliceKey) + .getBytes(); + byte[] aliceRevokedKey = sop.updateKey() + .mergeCerts(aliceRevokedCert) + .key(aliceKey) + .getBytes(); + + byte[] bobKey = sop.generateKey() + .userId("Bob ") + .generate() + .getBytes(); + byte[] bobCert = sop.extractCert() + .key(bobKey) + .getBytes(); + + assertThrows(SOPGPException.KeyCannotCertify.class, () -> + sop.certifyUserId() + .userId("Bob ") + .keys(aliceRevokedKey) + .certs(bobCert) + .getBytes()); + } } From 138e275bb6ca4241343d3bc50bec6ec55704d030 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 10:58:58 +0200 Subject: [PATCH 256/298] Fix formatting issues --- .../kotlin/sop/external/operation/UpdateKeyExternal.kt | 4 +++- sop-java/src/main/kotlin/sop/operation/UpdateKey.kt | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt index 99df15e..b84f452 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt @@ -21,7 +21,9 @@ class UpdateKeyExternal(binary: String, environment: Properties) : UpdateKey { override fun signingOnly(): UpdateKey = apply { commandList.add("--signing-only") } - override fun noAddedCapabilities(): UpdateKey = apply { commandList.add("--no-added-capabilities") } + override fun noAddedCapabilities(): UpdateKey = apply { + commandList.add("--no-added-capabilities") + } override fun withKeyPassword(password: ByteArray): UpdateKey = apply { commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount") diff --git a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt index 9a31310..1226ed5 100644 --- a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt @@ -20,14 +20,14 @@ interface UpdateKey { fun noArmor(): UpdateKey /** - * Allow key to be used for signing only. - * If this option is not present, the operation may add a new, encryption-capable component key. + * Allow key to be used for signing only. If this option is not present, the operation may add a + * new, encryption-capable component key. */ @Throws(SOPGPException.UnsupportedOption::class) fun signingOnly(): UpdateKey /** - * Do not allow adding new capabilities to the key. - * If this option is not present, the operation may add support for new capabilities to the key. + * Do not allow adding new capabilities to the key. If this option is not present, the operation + * may add support for new capabilities to the key. */ @Throws(SOPGPException.UnsupportedOption::class) fun noAddedCapabilities(): UpdateKey From be460fabab162a17dde5c4ad131cc1d8f17c12ca Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 6 May 2025 12:18:05 +0200 Subject: [PATCH 257/298] Add test for certifying without ASCII armor --- .../operation/CertifyValidateUserIdTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java index e05923c..c97fda7 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java @@ -73,6 +73,45 @@ public class CertifyValidateUserIdTest { "Alice accepts Bobs user-id after she certified it"); } + @ParameterizedTest + @MethodSource("provideInstances") + public void certifyUserIdUnarmored(SOP sop) throws IOException { + byte[] aliceKey = sop.generateKey() + .noArmor() + .withKeyPassword("sw0rdf1sh") + .userId("Alice ") + .generate() + .getBytes(); + byte[] aliceCert = sop.extractCert() + .noArmor() + .key(aliceKey) + .getBytes(); + + byte[] bobKey = sop.generateKey() + .noArmor() + .userId("Bob ") + .generate() + .getBytes(); + byte[] bobCert = sop.extractCert() + .noArmor() + .key(bobKey) + .getBytes(); + + byte[] bobCertifiedByAlice = sop.certifyUserId() + .noArmor() + .userId("Bob ") + .withKeyPassword("sw0rdf1sh") + .keys(aliceKey) + .certs(bobCert) + .getBytes(); + + assertTrue(sop.validateUserId() + .userId("Bob ") + .authorities(aliceCert) + .subjects(bobCertifiedByAlice), + "Alice accepts Bobs user-id after she certified it"); + } + @ParameterizedTest @MethodSource("provideInstances") public void addPetName(SOP sop) throws IOException { From 38c5a947dd5179b3b77b38bab8d0ae425e61fb9a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 May 2025 15:16:30 +0200 Subject: [PATCH 258/298] Add MergeCertsTest --- .../testsuite/operation/MergeCertsTest.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java new file mode 100644 index 0000000..04a083e --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import sop.SOP; +import sop.util.HexUtil; + +import java.io.IOException; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +public class MergeCertsTest extends AbstractSOPTest { + + static Stream provideInstances() { + return provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testMergeWithItself(SOP sop) throws IOException { + byte[] key = sop.generateKey() + .noArmor() + .userId("Alice ") + .generate() + .getBytes(); + + byte[] cert = sop.extractCert() + .noArmor() + .key(key) + .getBytes(); + + byte[] merged = sop.mergeCerts() + .noArmor() + .updates(cert) + .baseCertificates(cert) + .getBytes(); + + System.out.println("cert"); + System.out.println(new String(sop.armor().data(cert).getBytes())); + System.out.println("merged"); + System.out.println(new String(sop.armor().data(merged).getBytes())); + assertArrayEquals(cert, merged); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testApplyBaseToUpdate(SOP sop) throws IOException { + byte[] key = sop.generateKey() + .noArmor() + .userId("Alice ") + .generate() + .getBytes(); + + byte[] cert = sop.extractCert() + .noArmor() + .key(key) + .getBytes(); + + byte[] update = sop.revokeKey() + .noArmor() + .keys(key) + .getBytes(); + + byte[] merged = sop.mergeCerts() + .noArmor() + .updates(cert) + .baseCertificates(update) + .getBytes(); + + assertArrayEquals(update, merged); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testApplyUpdateToBase(SOP sop) throws IOException { + byte[] key = sop.generateKey() + .noArmor() + .userId("Alice ") + .generate() + .getBytes(); + + byte[] cert = sop.extractCert() + .noArmor() + .key(key) + .getBytes(); + + byte[] update = sop.revokeKey() + .noArmor() + .keys(key) + .getBytes(); + + byte[] merged = sop.mergeCerts() + .noArmor() + .updates(update) + .baseCertificates(cert) + .getBytes(); + + assertArrayEquals(update, merged); + } +} From 9360b0e8ce321edc6d274c6473b1b79622033f47 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 May 2025 15:36:36 +0200 Subject: [PATCH 259/298] Remove println statements --- .../src/main/java/sop/testsuite/operation/MergeCertsTest.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java index 04a083e..09387e9 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java @@ -41,10 +41,6 @@ public class MergeCertsTest extends AbstractSOPTest { .baseCertificates(cert) .getBytes(); - System.out.println("cert"); - System.out.println(new String(sop.armor().data(cert).getBytes())); - System.out.println("merged"); - System.out.println(new String(sop.armor().data(merged).getBytes())); assertArrayEquals(cert, merged); } From 6d23d3771d310e538e7a49b1ba92a46279c619c9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 May 2025 15:37:48 +0200 Subject: [PATCH 260/298] Remove unused import --- .../src/main/java/sop/testsuite/operation/MergeCertsTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java index 09387e9..3d40a99 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java @@ -8,7 +8,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import sop.SOP; -import sop.util.HexUtil; import java.io.IOException; import java.util.stream.Stream; From 77106942d1d4059b5b772aebbafa322604aa5f16 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 May 2025 00:36:16 +0200 Subject: [PATCH 261/298] Add tests for MergeCerts command --- .../testsuite/operation/MergeCertsTest.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java index 3d40a99..7bc99d1 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java @@ -4,6 +4,7 @@ package sop.testsuite.operation; +import kotlin.collections.ArraysKt; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -43,6 +44,52 @@ public class MergeCertsTest extends AbstractSOPTest { assertArrayEquals(cert, merged); } + @ParameterizedTest + @MethodSource("provideInstances") + public void testMergeWithItselfArmored(SOP sop) throws IOException { + byte[] key = sop.generateKey() + .noArmor() + .userId("Alice ") + .generate() + .getBytes(); + + byte[] cert = sop.extractCert() + .key(key) + .getBytes(); + + byte[] merged = sop.mergeCerts() + .updates(cert) + .baseCertificates(cert) + .getBytes(); + + assertArrayEquals(cert, merged); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testMergeWithItselfViaBase(SOP sop) throws IOException { + byte[] key = sop.generateKey() + .noArmor() + .userId("Alice ") + .generate() + .getBytes(); + + byte[] cert = sop.extractCert() + .noArmor() + .key(key) + .getBytes(); + + byte[] certs = ArraysKt.plus(cert, cert); + + byte[] merged = sop.mergeCerts() + .noArmor() + .updates(cert) + .baseCertificates(certs) + .getBytes(); + + assertArrayEquals(cert, merged); + } + @ParameterizedTest @MethodSource("provideInstances") public void testApplyBaseToUpdate(SOP sop) throws IOException { @@ -98,4 +145,39 @@ public class MergeCertsTest extends AbstractSOPTest { assertArrayEquals(update, merged); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testApplyUpdateToMissingBaseDoesNothing(SOP sop) throws IOException { + byte[] aliceKey = sop.generateKey() + .noArmor() + .userId("Alice ") + .generate() + .getBytes(); + + byte[] aliceCert = sop.extractCert() + .noArmor() + .key(aliceKey) + .getBytes(); + + byte[] bobKey = sop.generateKey() + .noArmor() + .userId("Bob ") + .generate() + .getBytes(); + + byte[] bobCert = sop.extractCert() + .noArmor() + .key(bobKey) + .getBytes(); + + byte[] merged = sop.mergeCerts() + .noArmor() + .updates(bobCert) + .baseCertificates(aliceCert) + .getBytes(); + + assertArrayEquals(aliceCert, merged); + } + } From 4ef5444e782cbfba98014311ed97425b83f29764 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 May 2025 01:09:36 +0200 Subject: [PATCH 262/298] Test key generation with supported profiles --- .../testsuite/operation/GenerateKeyTest.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java index 4a5da58..b63b4b8 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java @@ -9,6 +9,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 sop.Profile; import sop.SOP; import sop.exception.SOPGPException; import sop.testsuite.JUtils; @@ -16,9 +17,11 @@ import sop.testsuite.TestData; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") public class GenerateKeyTest extends AbstractSOPTest { @@ -118,4 +121,30 @@ public class GenerateKeyTest extends AbstractSOPTest { sop.encrypt().withCert(signingOnlyCert) .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8))); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void generateKeyWithSupportedProfiles(SOP sop) throws IOException { + List profiles = sop.listProfiles() + .generateKey(); + + for (Profile profile : profiles) { + generateKeyWithProfile(sop, profile.getName()); + } + } + + private void generateKeyWithProfile(SOP sop, String profile) throws IOException { + byte[] key; + try { + key = sop.generateKey() + .profile(profile) + .userId("Alice ") + .generate() + .getBytes(); + } catch (SOPGPException.UnsupportedProfile e) { + key = null; + } + assumeTrue(key != null, "'generate-key' does not support profile '" + profile + "'."); + JUtils.assertArrayStartsWith(key, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); + } } From e5cb58468b22cd8945fd8151d74bb4cf00cb922e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 27 May 2025 18:11:54 +0200 Subject: [PATCH 263/298] Add aliases to Profile --- sop-java/src/main/kotlin/sop/Profile.kt | 44 ++++++++++++++++----- sop-java/src/test/java/sop/ProfileTest.java | 21 ++++++++++ 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/Profile.kt b/sop-java/src/main/kotlin/sop/Profile.kt index 2125c57..2208592 100644 --- a/sop-java/src/main/kotlin/sop/Profile.kt +++ b/sop-java/src/main/kotlin/sop/Profile.kt @@ -20,11 +20,15 @@ import sop.util.UTF8Util * in the IETF namespace that begins with the string `draft-` should have semantics that hew as * closely as possible to the referenced Internet Draft. * @param description a free-form description of the profile. - * @see - * SOP Spec - Profile + * @param aliases list of optional profile alias names + * @see + * [SOP Spec - Profile](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html#name-profile) */ -data class Profile(val name: String, val description: Optional) { +data class Profile( + val name: String, + val description: Optional, + val aliases: List = listOf() +) { @JvmOverloads constructor( @@ -50,8 +54,18 @@ data class Profile(val name: String, val description: Optional) { * * @return string */ - override fun toString(): String = - if (description.isEmpty) name else "$name: ${description.get()}" + override fun toString(): String = buildString { + append(name) + if (!description.isEmpty || !aliases.isEmpty()) { + append(":") + } + if (!description.isEmpty) { + append(" ${description.get()}") + } + if (!aliases.isEmpty()) { + append(" (aliases: ${aliases.joinToString(separator = ", ")})") + } + } companion object { @@ -64,9 +78,21 @@ data class Profile(val name: String, val description: Optional) { @JvmStatic fun parse(string: String): Profile { return if (string.contains(": ")) { - Profile( - string.substring(0, string.indexOf(": ")), - string.substring(string.indexOf(": ") + 2).trim()) + val name = string.substring(0, string.indexOf(": ")) + var description = string.substring(string.indexOf(": ") + 2).trim() + if (description.contains("(aliases: ")) { + val aliases = + description.substring( + description.indexOf("(aliases: ") + 10, description.indexOf(")")) + description = description.substring(0, description.indexOf("(aliases: ")).trim() + Profile(name, Optional.of(description), aliases.split(", ").toList()) + } else { + if (description.isNotBlank()) { + Profile(name, Optional.of(description)) + } else { + Profile(name) + } + } } else if (string.endsWith(":")) { Profile(string.substring(0, string.length - 1)) } else { diff --git a/sop-java/src/test/java/sop/ProfileTest.java b/sop-java/src/test/java/sop/ProfileTest.java index 564a6af..770b88b 100644 --- a/sop-java/src/test/java/sop/ProfileTest.java +++ b/sop-java/src/test/java/sop/ProfileTest.java @@ -5,6 +5,9 @@ package sop; import org.junit.jupiter.api.Test; +import sop.util.Optional; + +import java.util.Arrays; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -19,6 +22,24 @@ public class ProfileTest { assertEquals("default: Use the implementers recommendations.", profile.toString()); } + @Test + public void withAliasesToString() { + Profile profile = new Profile( + "Foo", + Optional.of("Something something"), + Arrays.asList("Bar", "Baz")); + assertEquals("Foo: Something something (aliases: Bar, Baz)", profile.toString()); + } + + + @Test + public void parseWithAliases() { + Profile profile = Profile.parse("Foo: Something something (aliases: Bar, Baz)"); + assertEquals("Foo", profile.getName()); + assertEquals("Something something", profile.getDescription().get()); + assertEquals(Arrays.asList("Bar", "Baz"), profile.getAliases()); + } + @Test public void toStringNameOnly() { Profile profile = new Profile("default"); From 9677f1fd0b3a12332c08c3f03a3458f5ea5e14be Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 27 May 2025 18:34:04 +0200 Subject: [PATCH 264/298] Fix profile constructors --- sop-java/src/main/kotlin/sop/Profile.kt | 9 +++++---- sop-java/src/test/java/sop/ProfileTest.java | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/Profile.kt b/sop-java/src/main/kotlin/sop/Profile.kt index 2208592..725bbe5 100644 --- a/sop-java/src/main/kotlin/sop/Profile.kt +++ b/sop-java/src/main/kotlin/sop/Profile.kt @@ -33,8 +33,9 @@ data class Profile( @JvmOverloads constructor( name: String, - description: String? = null - ) : this(name, Optional.ofNullable(description?.trim()?.ifBlank { null })) + description: String? = null, + aliases: List = listOf() + ) : this(name, Optional.ofNullable(description?.trim()?.ifBlank { null }), aliases) init { require(name.trim().isNotBlank()) { "Name cannot be empty." } @@ -85,10 +86,10 @@ data class Profile( description.substring( description.indexOf("(aliases: ") + 10, description.indexOf(")")) description = description.substring(0, description.indexOf("(aliases: ")).trim() - Profile(name, Optional.of(description), aliases.split(", ").toList()) + Profile(name, description, aliases.split(", ").toList()) } else { if (description.isNotBlank()) { - Profile(name, Optional.of(description)) + Profile(name, description) } else { Profile(name) } diff --git a/sop-java/src/test/java/sop/ProfileTest.java b/sop-java/src/test/java/sop/ProfileTest.java index 770b88b..41b4a2f 100644 --- a/sop-java/src/test/java/sop/ProfileTest.java +++ b/sop-java/src/test/java/sop/ProfileTest.java @@ -31,7 +31,6 @@ public class ProfileTest { assertEquals("Foo: Something something (aliases: Bar, Baz)", profile.toString()); } - @Test public void parseWithAliases() { Profile profile = Profile.parse("Foo: Something something (aliases: Bar, Baz)"); From e4817174217697bcaf33d227c76f36600d13be6c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 30 May 2025 12:48:24 +0200 Subject: [PATCH 265/298] Add Profile.withAliases() utility function --- sop-java/src/main/kotlin/sop/Profile.kt | 10 ++++++++++ sop-java/src/test/java/sop/ProfileTest.java | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/sop-java/src/main/kotlin/sop/Profile.kt b/sop-java/src/main/kotlin/sop/Profile.kt index 725bbe5..fd58c63 100644 --- a/sop-java/src/main/kotlin/sop/Profile.kt +++ b/sop-java/src/main/kotlin/sop/Profile.kt @@ -50,6 +50,16 @@ data class Profile( fun hasDescription() = description.isPresent + /** + * Return a copy of this [Profile] with the aliases set to the given strings. + * + * @param alias one or more alias names + * @return profile with aliases + */ + fun withAliases(vararg alias: String): Profile { + return Profile(name, description, alias.toList()) + } + /** * Convert the profile into a String for displaying. * diff --git a/sop-java/src/test/java/sop/ProfileTest.java b/sop-java/src/test/java/sop/ProfileTest.java index 41b4a2f..f418672 100644 --- a/sop-java/src/test/java/sop/ProfileTest.java +++ b/sop-java/src/test/java/sop/ProfileTest.java @@ -39,6 +39,20 @@ public class ProfileTest { assertEquals(Arrays.asList("Bar", "Baz"), profile.getAliases()); } + @Test + public void changeAliasesWithWithAliases() { + Profile p = new Profile("Foo", "Bar any Baz", Arrays.asList("tinitus", "particle")); + p = p.withAliases("fnord", "qbit"); + + assertEquals("Foo", p.getName()); + assertEquals("Bar any Baz", p.getDescription().get()); + + assertTrue(p.getAliases().contains("fnord")); + assertTrue(p.getAliases().contains("qbit")); + assertFalse(p.getAliases().contains("tinitus")); + assertFalse(p.getAliases().contains("particle")); + } + @Test public void toStringNameOnly() { Profile profile = new Profile("default"); From c5d9e57f69073fb5c320d7aff3c6f68c3d99b1fa Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 30 May 2025 15:05:44 +0200 Subject: [PATCH 266/298] Add test for encrypt-decrypt using all available generate-key profiles --- .../operation/EncryptDecryptTest.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java index df824ca..330ca90 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java @@ -11,12 +11,15 @@ import org.junit.jupiter.params.provider.MethodSource; import sop.ByteArrayAndResult; import sop.DecryptionResult; import sop.EncryptionResult; +import sop.Profile; import sop.SOP; import sop.SessionKey; import sop.Verification; import sop.enums.EncryptAs; import sop.enums.SignatureMode; import sop.exception.SOPGPException; +import sop.operation.Decrypt; +import sop.operation.Encrypt; import sop.testsuite.TestData; import sop.testsuite.assertions.VerificationListAssert; import sop.util.Optional; @@ -25,6 +28,7 @@ import sop.util.UTCUtil; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.text.ParseException; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.stream.Stream; @@ -338,4 +342,55 @@ public class EncryptDecryptTest extends AbstractSOPTest { .toByteArrayAndResult() .getBytes()); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void encryptDecryptWithAllSupportedKeyGenerationProfiles(SOP sop) throws IOException { + List profiles = sop.listProfiles().generateKey(); + + List keys = new ArrayList<>(); + List certs = new ArrayList<>(); + for (Profile p : profiles) { + byte[] k = sop.generateKey() + .profile(p) + .userId(p.getName()) + .generate() + .getBytes(); + keys.add(k); + + byte[] c = sop.extractCert() + .key(k) + .getBytes(); + certs.add(c); + } + + byte[] plaintext = "Hello, World!\n".getBytes(); + + Encrypt encrypt = sop.encrypt(); + for (byte[] c : certs) { + encrypt.withCert(c); + } + for (byte[] k : keys) { + encrypt.signWith(k); + } + + ByteArrayAndResult encRes = encrypt.plaintext(plaintext) + .toByteArrayAndResult(); + EncryptionResult eResult = encRes.getResult(); + byte[] ciphertext = encRes.getBytes(); + + for (byte[] k : keys) { + Decrypt decrypt = sop.decrypt() + .withKey(k); + for (byte[] c : certs) { + decrypt.verifyWithCert(c); + } + ByteArrayAndResult decRes = decrypt.ciphertext(ciphertext) + .toByteArrayAndResult(); + DecryptionResult dResult = decRes.getResult(); + byte[] decPlaintext = decRes.getBytes(); + assertArrayEquals(plaintext, decPlaintext); + assertEquals(certs.size(), dResult.getVerifications().size()); + } + } } From 00a02686c8e44e748faf96ca7d58abbb0c153f6c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 12:21:07 +0200 Subject: [PATCH 267/298] External-SOP: Fix command names --- .../main/kotlin/sop/external/operation/CertifyUserIdExternal.kt | 2 +- .../main/kotlin/sop/external/operation/MergeCertsExternal.kt | 2 +- .../kotlin/sop/external/operation/ValidateUserIdExternal.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt index abf4d50..c6c08cd 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt @@ -12,7 +12,7 @@ import sop.operation.CertifyUserId class CertifyUserIdExternal(binary: String, environment: Properties) : CertifyUserId { - private val commandList = mutableListOf(binary, "version") + private val commandList = mutableListOf(binary, "certify-userid") private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() private var argCount = 0 diff --git a/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt index 0869fab..b739eb3 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt @@ -12,7 +12,7 @@ import sop.operation.MergeCerts class MergeCertsExternal(binary: String, environment: Properties) : MergeCerts { - private val commandList = mutableListOf(binary, "version") + private val commandList = mutableListOf(binary, "merge-certs") private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() private var argCount = 0 diff --git a/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt index 581d6f5..cf4742b 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt @@ -12,7 +12,7 @@ import sop.util.UTCUtil class ValidateUserIdExternal(binary: String, environment: Properties) : ValidateUserId { - private val commandList = mutableListOf(binary, "version") + private val commandList = mutableListOf(binary, "validate-userid") private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() private var argCount = 0 From 0df80470c682fe939727836463892ef98606891f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 12:21:31 +0200 Subject: [PATCH 268/298] External-SOP: Extend test suite with new test classes --- .../ExternalCertifyValidateUserIdTest.java | 13 +++++++++++++ .../operation/ExternalChangeKeyPasswordTest.java | 13 +++++++++++++ .../external/operation/ExternalMergeCertsTest.java | 13 +++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java create mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java create mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java new file mode 100644 index 0000000..bb319ca --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.external.operation; + +import org.junit.jupiter.api.condition.EnabledIf; +import sop.testsuite.operation.CertifyValidateUserIdTest; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class ExternalCertifyValidateUserIdTest extends CertifyValidateUserIdTest { + +} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java new file mode 100644 index 0000000..42a9693 --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.external.operation; + +import org.junit.jupiter.api.condition.EnabledIf; +import sop.testsuite.operation.ChangeKeyPasswordTest; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class ExternalChangeKeyPasswordTest extends ChangeKeyPasswordTest { + +} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java new file mode 100644 index 0000000..8b22b37 --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.external.operation; + +import org.junit.jupiter.api.condition.EnabledIf; +import sop.testsuite.operation.MergeCertsTest; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class ExternalMergeCertsTest extends MergeCertsTest { + +} From 47a6db8702d382ca2411393e456879deba27162e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 12:53:24 +0200 Subject: [PATCH 269/298] External-SOP: Fix error message typo --- 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 8ab7737..1291433 100644 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -182,7 +182,7 @@ class ExternalSOP( "External SOP backend reported error NoHardwareKeyFound ($exitCode):\n$errorMessage") HardwareKeyFailure.EXIT_CODE -> throw HardwareKeyFailure( - "External SOP backend reported error HardwareKeyFalure ($exitCode):\n$errorMessage") + "External SOP backend reported error HardwareKeyFailure ($exitCode):\n$errorMessage") PrimaryKeyBad.EXIT_CODE -> throw PrimaryKeyBad( "External SOP backend reported error PrimaryKeyBad ($exitCode):\n$errorMessage") From 589884672a37a3ba7b444bd1d539b73fb55bd6dd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 12:53:46 +0200 Subject: [PATCH 270/298] External-SOP: Properly map KeyCannotCertify error code --- external-sop/src/main/kotlin/sop/external/ExternalSOP.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt index 1291433..b43b786 100644 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -189,6 +189,9 @@ class ExternalSOP( CertUserIdNoMatch.EXIT_CODE -> throw CertUserIdNoMatch( "External SOP backend reported error CertUserIdNoMatch ($exitCode):\n$errorMessage") + KeyCannotCertify.EXIT_CODE -> + throw KeyCannotCertify( + "External SOP backend reported error KeyCannotCertify ($exitCode):\n$errorMessage") // Did you forget to add a case for a new exception type? else -> From 61206dde5310a1cec33a2f8698885f32ccc46b97 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 12:54:41 +0200 Subject: [PATCH 271/298] GenerateKeyTest: Provoke exception for CertCannotEncrypt test case --- .../main/java/sop/testsuite/operation/GenerateKeyTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java index b63b4b8..0d8b78e 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java @@ -119,7 +119,9 @@ public class GenerateKeyTest extends AbstractSOPTest { assertThrows(SOPGPException.CertCannotEncrypt.class, () -> sop.encrypt().withCert(signingOnlyCert) - .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8))); + .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) + .toByteArrayAndResult() + .getBytes()); } @ParameterizedTest From e1d048225b04c272632e076224325367c769d74d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 12:57:11 +0200 Subject: [PATCH 272/298] CertifyValidateUserIdTest: unbound User-IDs do throw exceptions --- .../operation/CertifyValidateUserIdTest.java | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java index c97fda7..7f9f088 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java @@ -14,7 +14,6 @@ import sop.exception.SOPGPException; import java.io.IOException; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -47,16 +46,17 @@ public class CertifyValidateUserIdTest { // Alice has her own user-id self-certified assertTrue(sop.validateUserId() - .authorities(aliceCert) - .userId("Alice ") - .subjects(aliceCert), + .authorities(aliceCert) + .userId("Alice ") + .subjects(aliceCert), "Alice accepts her own self-certified user-id"); // Alice has not yet certified Bobs user-id - assertFalse(sop.validateUserId() - .authorities(aliceCert) - .userId("Bob ") - .subjects(bobCert), + assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> + sop.validateUserId() + .authorities(aliceCert) + .userId("Bob ") + .subjects(bobCert), "Alice has not yet certified Bobs user-id"); byte[] bobCertifiedByAlice = sop.certifyUserId() @@ -67,10 +67,10 @@ public class CertifyValidateUserIdTest { .getBytes(); assertTrue(sop.validateUserId() - .userId("Bob ") - .authorities(aliceCert) - .subjects(bobCertifiedByAlice), - "Alice accepts Bobs user-id after she certified it"); + .userId("Bob ") + .authorities(aliceCert) + .subjects(bobCertifiedByAlice), + "Alice accepts Bobs user-id after she certified it"); } @ParameterizedTest @@ -132,11 +132,11 @@ public class CertifyValidateUserIdTest { .getBytes(); assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> - sop.certifyUserId() - .userId("Bobby") - .keys(aliceKey) - .certs(bobCert) - .getBytes(), + sop.certifyUserId() + .userId("Bobby") + .keys(aliceKey) + .certs(bobCert) + .getBytes(), "Alice cannot create a pet-name for Bob without the --no-require-self-sig flag"); byte[] bobWithPetName = sop.certifyUserId() @@ -147,15 +147,16 @@ public class CertifyValidateUserIdTest { .getBytes(); assertTrue(sop.validateUserId() - .userId("Bobby") - .authorities(aliceCert) - .subjects(bobWithPetName), + .userId("Bobby") + .authorities(aliceCert) + .subjects(bobWithPetName), "Alice accepts the pet-name she gave to Bob"); - assertFalse(sop.validateUserId() - .userId("Bobby") - .authorities(bobWithPetName) - .subjects(bobWithPetName), + assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> + sop.validateUserId() + .userId("Bobby") + .authorities(bobWithPetName) + .subjects(bobWithPetName), "Bob does not accept the pet-name Alice gave him"); } From ab13cc1de16a44daa2fde0ad6dde33ac8351d5d7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 13:39:31 +0200 Subject: [PATCH 273/298] CertifyUserIdExternal: add separator before passing keys --- .../main/kotlin/sop/external/operation/CertifyUserIdExternal.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt index c6c08cd..e3661db 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt @@ -44,5 +44,5 @@ class CertifyUserIdExternal(binary: String, environment: Properties) : CertifyUs override fun certs(certs: InputStream): Ready = ExternalSOP.executeTransformingOperation( - Runtime.getRuntime(), commandList.plus(keys), envList, certs) + Runtime.getRuntime(), commandList.plus("--").plus(keys), envList, certs) } From 28d06c330d201815b2662f2b6238f647337e4a97 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 13:47:20 +0200 Subject: [PATCH 274/298] ExternalSOP: Map UnspecificError --- external-sop/src/main/kotlin/sop/external/ExternalSOP.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt index b43b786..48a5af9 100644 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -120,6 +120,9 @@ class ExternalSOP( val errorMessage = readString(errIn) when (exitCode) { + UnspecificFailure.EXIT_CODE -> + throw UnspecificFailure( + "External SOP backend reported an unspecific error ($exitCode):\n$errorMessage") NoSignature.EXIT_CODE -> throw NoSignature( "External SOP backend reported error NoSignature ($exitCode):\n$errorMessage") From 79aece6f04059983139615afde3edaf336703760 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 14:13:36 +0200 Subject: [PATCH 275/298] SOP, SOPV: Add --debug option --- sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt | 2 +- sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt | 2 +- 2 files changed, 2 insertions(+), 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 98701fc..07caa03 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 @@ -64,7 +64,7 @@ class SopCLI { @JvmField var EXECUTABLE_NAME = "sop" @JvmField - @Option(names = ["--stacktrace"], scope = ScopeType.INHERIT) + @Option(names = ["--stacktrace", "--debug"], scope = ScopeType.INHERIT) var stacktrace = false @JvmStatic 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 index 9a8b4b4..fa9683f 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt @@ -45,7 +45,7 @@ class SopVCLI { @JvmField var EXECUTABLE_NAME = "sopv" @JvmField - @CommandLine.Option(names = ["--stacktrace"], scope = CommandLine.ScopeType.INHERIT) + @CommandLine.Option(names = ["--stacktrace", "--debug"], scope = CommandLine.ScopeType.INHERIT) var stacktrace = false @JvmStatic From 5a7a8ae901f58f0bdc6feacc70115d370834bc7f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 14:14:22 +0200 Subject: [PATCH 276/298] MergeCertsTest: do not pass unarmored data This is done to fix external-sop tests, which rely on environment variables, which do not play nicely with binary data --- .../testsuite/operation/MergeCertsTest.java | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java index 7bc99d1..b577017 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java @@ -25,18 +25,15 @@ public class MergeCertsTest extends AbstractSOPTest { @MethodSource("provideInstances") public void testMergeWithItself(SOP sop) throws IOException { byte[] key = sop.generateKey() - .noArmor() .userId("Alice ") .generate() .getBytes(); byte[] cert = sop.extractCert() - .noArmor() .key(key) .getBytes(); byte[] merged = sop.mergeCerts() - .noArmor() .updates(cert) .baseCertificates(cert) .getBytes(); @@ -69,20 +66,17 @@ public class MergeCertsTest extends AbstractSOPTest { @MethodSource("provideInstances") public void testMergeWithItselfViaBase(SOP sop) throws IOException { byte[] key = sop.generateKey() - .noArmor() .userId("Alice ") .generate() .getBytes(); byte[] cert = sop.extractCert() - .noArmor() .key(key) .getBytes(); byte[] certs = ArraysKt.plus(cert, cert); byte[] merged = sop.mergeCerts() - .noArmor() .updates(cert) .baseCertificates(certs) .getBytes(); @@ -94,23 +88,19 @@ public class MergeCertsTest extends AbstractSOPTest { @MethodSource("provideInstances") public void testApplyBaseToUpdate(SOP sop) throws IOException { byte[] key = sop.generateKey() - .noArmor() .userId("Alice ") .generate() .getBytes(); byte[] cert = sop.extractCert() - .noArmor() .key(key) .getBytes(); byte[] update = sop.revokeKey() - .noArmor() .keys(key) .getBytes(); byte[] merged = sop.mergeCerts() - .noArmor() .updates(cert) .baseCertificates(update) .getBytes(); @@ -122,23 +112,19 @@ public class MergeCertsTest extends AbstractSOPTest { @MethodSource("provideInstances") public void testApplyUpdateToBase(SOP sop) throws IOException { byte[] key = sop.generateKey() - .noArmor() .userId("Alice ") .generate() .getBytes(); byte[] cert = sop.extractCert() - .noArmor() .key(key) .getBytes(); byte[] update = sop.revokeKey() - .noArmor() .keys(key) .getBytes(); byte[] merged = sop.mergeCerts() - .noArmor() .updates(update) .baseCertificates(cert) .getBytes(); @@ -150,29 +136,24 @@ public class MergeCertsTest extends AbstractSOPTest { @MethodSource("provideInstances") public void testApplyUpdateToMissingBaseDoesNothing(SOP sop) throws IOException { byte[] aliceKey = sop.generateKey() - .noArmor() .userId("Alice ") .generate() .getBytes(); byte[] aliceCert = sop.extractCert() - .noArmor() .key(aliceKey) .getBytes(); byte[] bobKey = sop.generateKey() - .noArmor() .userId("Bob ") .generate() .getBytes(); byte[] bobCert = sop.extractCert() - .noArmor() .key(bobKey) .getBytes(); byte[] merged = sop.mergeCerts() - .noArmor() .updates(bobCert) .baseCertificates(aliceCert) .getBytes(); From bff4423f93ce0897f52598cc8e6805bd6332301c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 3 Jun 2025 14:30:54 +0200 Subject: [PATCH 277/298] Verification: Rename description to jsonOrDescription --- .../sop/testsuite/assertions/VerificationAssert.java | 4 ++-- sop-java/src/main/kotlin/sop/Verification.kt | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java index 63fd237..5267148 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java @@ -45,12 +45,12 @@ public final class VerificationAssert { } public VerificationAssert hasDescription(String description) { - assertEquals(description, verification.getDescription().get()); + assertEquals(description, verification.getJsonOrDescription().get()); return this; } public VerificationAssert hasDescriptionOrNull(String description) { - if (verification.getDescription().isEmpty()) { + if (verification.getJsonOrDescription().isEmpty()) { return this; } diff --git a/sop-java/src/main/kotlin/sop/Verification.kt b/sop-java/src/main/kotlin/sop/Verification.kt index 20401a9..65a858e 100644 --- a/sop-java/src/main/kotlin/sop/Verification.kt +++ b/sop-java/src/main/kotlin/sop/Verification.kt @@ -15,7 +15,7 @@ data class Verification( val signingKeyFingerprint: String, val signingCertFingerprint: String, val signatureMode: Optional, - val description: Optional + val jsonOrDescription: Optional ) { @JvmOverloads constructor( @@ -31,10 +31,15 @@ data class Verification( Optional.ofNullable(signatureMode), Optional.ofNullable(description?.trim())) + @Deprecated("Replaced by jsonOrDescription", + replaceWith = ReplaceWith("jsonOrDescription") + ) + val description = jsonOrDescription + override fun toString(): String = "${UTCUtil.formatUTCDate(creationTime)} $signingKeyFingerprint $signingCertFingerprint" + (if (signatureMode.isPresent) " mode:${signatureMode.get()}" else "") + - (if (description.isPresent) " ${description.get()}" else "") + (if (jsonOrDescription.isPresent) " ${jsonOrDescription.get()}" else "") companion object { @JvmStatic From ebfde3542228c9e94b6aae20e99dda30fbe2c4d3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 3 Jun 2025 14:31:07 +0200 Subject: [PATCH 278/298] Add support for JSON POJOs --- sop-java/src/main/kotlin/sop/Verification.kt | 78 ++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/sop-java/src/main/kotlin/sop/Verification.kt b/sop-java/src/main/kotlin/sop/Verification.kt index 65a858e..a8db800 100644 --- a/sop-java/src/main/kotlin/sop/Verification.kt +++ b/sop-java/src/main/kotlin/sop/Verification.kt @@ -17,6 +17,7 @@ data class Verification( val signatureMode: Optional, val jsonOrDescription: Optional ) { + @JvmOverloads constructor( creationTime: Date, @@ -31,11 +32,44 @@ data class Verification( Optional.ofNullable(signatureMode), Optional.ofNullable(description?.trim())) + @JvmOverloads + constructor( + creationTime: Date, + signingKeyFingerprint: String, + signingCertFingerprint: String, + signatureMode: SignatureMode? = null, + json: JSON, + jsonSerializer: JSONSerializer + ) : this( + creationTime, + signingKeyFingerprint, + signingCertFingerprint, + Optional.ofNullable(signatureMode), + Optional.of(jsonSerializer.serialize(json))) + @Deprecated("Replaced by jsonOrDescription", replaceWith = ReplaceWith("jsonOrDescription") ) val description = jsonOrDescription + /** + * Attempt to parse the [jsonOrDescription] field using the provided [JSONParser] and return the result. + * This method returns `null` if parsing fails. + * + * @param parser [JSONParser] implementation + * @return successfully parsed [JSON] POJO or `null`. + */ + fun getJson(parser: JSONParser): JSON? { + return jsonOrDescription.get() + ?.let { + try { + parser.parse(it) + } catch (e: ParseException) { + null + } + } + } + override fun toString(): String = "${UTCUtil.formatUTCDate(creationTime)} $signingKeyFingerprint $signingCertFingerprint" + (if (signatureMode.isPresent) " mode:${signatureMode.get()}" else "") + @@ -78,4 +112,48 @@ data class Verification( } } } + + /** + * POJO data class representing JSON metadata. + * + * @param signers list of supplied CERTS objects that could have issued the signature, identified by + * the name given on the command line. + * @param comment a freeform UTF-8 encoded text describing the verification + * @param ext an extension object containing arbitrary, implementation-specific data + */ + data class JSON( + val signers: List, + val comment: String?, + val ext: Any?) + + /** + * Interface abstracting a JSON parser that parses [JSON] POJOs from single-line strings. + */ + fun interface JSONParser { + /** + * Parse a [JSON] POJO from the given single-line [string]. + * If the string does not represent a JSON object matching the [JSON] definition, + * this method throws a [ParseException]. + * + * @param string [String] representation of the [JSON] object. + * @return parsed [JSON] POJO + * @throws ParseException if the [string] is not a JSON string representing the [JSON] object. + */ + @Throws(ParseException::class) + fun parse(string: String): JSON + } + + /** + * Interface abstracting a JSON serializer that converts [JSON] POJOs into single-line JSON strings. + */ + fun interface JSONSerializer { + + /** + * Serialize the given [JSON] object into a single-line JSON string. + * + * @param json JSON POJO + * @return JSON string + */ + fun serialize(json: JSON): String + } } From cdcbae7e5f4ecd448a3cabfbd3367f99d6f694f2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 3 Jun 2025 23:14:23 +0200 Subject: [PATCH 279/298] Add test for JSON data parsing and serializing using a dummy implementation --- .../main/kotlin/sop/cli/picocli/SopVCLI.kt | 3 +- sop-java/src/main/kotlin/sop/Verification.kt | 68 +++++--- .../test/java/sop/VerificationJSONTest.java | 163 ++++++++++++++++++ .../src/test/java/sop/VerificationTest.java | 3 + 4 files changed, 208 insertions(+), 29 deletions(-) create mode 100644 sop-java/src/test/java/sop/VerificationJSONTest.java 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 index fa9683f..311a446 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt @@ -45,7 +45,8 @@ class SopVCLI { @JvmField var EXECUTABLE_NAME = "sopv" @JvmField - @CommandLine.Option(names = ["--stacktrace", "--debug"], scope = CommandLine.ScopeType.INHERIT) + @CommandLine.Option( + names = ["--stacktrace", "--debug"], scope = CommandLine.ScopeType.INHERIT) var stacktrace = false @JvmStatic diff --git a/sop-java/src/main/kotlin/sop/Verification.kt b/sop-java/src/main/kotlin/sop/Verification.kt index a8db800..982e691 100644 --- a/sop-java/src/main/kotlin/sop/Verification.kt +++ b/sop-java/src/main/kotlin/sop/Verification.kt @@ -10,6 +10,15 @@ import sop.enums.SignatureMode import sop.util.Optional import sop.util.UTCUtil +/** + * Metadata about a verified signature. + * + * @param creationTime creation time of the signature + * @param signingKeyFingerprint fingerprint of the (sub-)key that issued the signature + * @param signingCertFingerprint fingerprint of the certificate that contains the signing key + * @param signatureMode optional signature mode (text/binary) + * @param jsonOrDescription arbitrary text or JSON data + */ data class Verification( val creationTime: Date, val signingKeyFingerprint: String, @@ -47,27 +56,28 @@ data class Verification( Optional.ofNullable(signatureMode), Optional.of(jsonSerializer.serialize(json))) - @Deprecated("Replaced by jsonOrDescription", - replaceWith = ReplaceWith("jsonOrDescription") - ) + @Deprecated("Replaced by jsonOrDescription", replaceWith = ReplaceWith("jsonOrDescription")) val description = jsonOrDescription + /** This value is `true` if the [Verification] contains extension JSON. */ + val containsJson: Boolean = + jsonOrDescription.get()?.trim()?.let { it.startsWith("{") && it.endsWith("}") } ?: false + /** - * Attempt to parse the [jsonOrDescription] field using the provided [JSONParser] and return the result. - * This method returns `null` if parsing fails. + * Attempt to parse the [jsonOrDescription] field using the provided [JSONParser] and return the + * result. This method returns `null` if parsing fails. * * @param parser [JSONParser] implementation * @return successfully parsed [JSON] POJO or `null`. */ fun getJson(parser: JSONParser): JSON? { - return jsonOrDescription.get() - ?.let { - try { - parser.parse(it) - } catch (e: ParseException) { - null - } + return jsonOrDescription.get()?.let { + try { + parser.parse(it) + } catch (e: ParseException) { + null } + } } override fun toString(): String = @@ -116,35 +126,37 @@ data class Verification( /** * POJO data class representing JSON metadata. * - * @param signers list of supplied CERTS objects that could have issued the signature, identified by - * the name given on the command line. + * @param signers list of supplied CERTS objects that could have issued the signature, + * identified by the name given on the command line. * @param comment a freeform UTF-8 encoded text describing the verification * @param ext an extension object containing arbitrary, implementation-specific data */ - data class JSON( - val signers: List, - val comment: String?, - val ext: Any?) + data class JSON(val signers: List, val comment: String?, val ext: Any?) { - /** - * Interface abstracting a JSON parser that parses [JSON] POJOs from single-line strings. - */ + /** Create a JSON object with only a list of signers. */ + constructor(signers: List) : this(signers, null, null) + + /** Create a JSON object with only a single signer. */ + constructor(signer: String) : this(listOf(signer)) + } + + /** Interface abstracting a JSON parser that parses [JSON] POJOs from single-line strings. */ fun interface JSONParser { /** - * Parse a [JSON] POJO from the given single-line [string]. - * If the string does not represent a JSON object matching the [JSON] definition, - * this method throws a [ParseException]. + * Parse a [JSON] POJO from the given single-line [string]. If the string does not represent + * a JSON object matching the [JSON] definition, this method throws a [ParseException]. * * @param string [String] representation of the [JSON] object. * @return parsed [JSON] POJO - * @throws ParseException if the [string] is not a JSON string representing the [JSON] object. + * @throws ParseException if the [string] is not a JSON string representing the [JSON] + * object. */ - @Throws(ParseException::class) - fun parse(string: String): JSON + @Throws(ParseException::class) fun parse(string: String): JSON } /** - * Interface abstracting a JSON serializer that converts [JSON] POJOs into single-line JSON strings. + * Interface abstracting a JSON serializer that converts [JSON] POJOs into single-line JSON + * strings. */ fun interface JSONSerializer { diff --git a/sop-java/src/test/java/sop/VerificationJSONTest.java b/sop-java/src/test/java/sop/VerificationJSONTest.java new file mode 100644 index 0000000..10253d8 --- /dev/null +++ b/sop-java/src/test/java/sop/VerificationJSONTest.java @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import sop.enums.SignatureMode; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class VerificationJSONTest { + + // A hacky self-made "JSON parser" stand-in. + // Only used for testing, do not use in production! + private Verification.JSONParser dummyParser = new Verification.JSONParser() { + @NotNull + @Override + public Verification.JSON parse(@NotNull String string) throws ParseException { + if (!string.startsWith("{")) { + throw new ParseException("Alleged JSON String does not begin with '{'", 0); + } + if (!string.endsWith("}")) { + throw new ParseException("Alleged JSON String does not end with '}'", string.length() - 1); + } + + List signersList = new ArrayList<>(); + Matcher signersMat = Pattern.compile("\"signers\": \\[(.*?)\\]").matcher(string); + if (signersMat.find()) { + String signersCat = signersMat.group(1); + String[] split = signersCat.split(","); + for (String s : split) { + s = s.trim(); + signersList.add(s.substring(1, s.length() - 1)); + } + } + + String comment = null; + Matcher commentMat = Pattern.compile("\"comment\": \"(.*?)\"").matcher(string); + if (commentMat.find()) { + comment = commentMat.group(1); + } + + String ext = null; + Matcher extMat = Pattern.compile("\"ext\": (.*?})}").matcher(string); + if (extMat.find()) { + ext = extMat.group(1); + } + + return new Verification.JSON(signersList, comment, ext); + } + }; + + // A just as hacky "JSON Serializer" lookalike. + // Also don't use in production, for testing only! + private Verification.JSONSerializer dummySerializer = new Verification.JSONSerializer() { + @NotNull + @Override + public String serialize(@NotNull Verification.JSON json) { + if (json.getSigners().isEmpty() && json.getComment() == null && json.getExt() == null) { + return ""; + } + StringBuilder sb = new StringBuilder("{"); + boolean comma = false; + + if (!json.getSigners().isEmpty()) { + comma = true; + sb.append("\"signers\": ["); + for (Iterator iterator = json.getSigners().iterator(); iterator.hasNext(); ) { + String signer = iterator.next(); + sb.append("\"").append(signer).append("\""); + if (iterator.hasNext()) { + sb.append(", "); + } + } + sb.append("]"); + } + + if (json.getComment() != null) { + if (comma) { + sb.append(", "); + } + comma = true; + sb.append("\"comment\": \"").append(json.getComment()).append("\""); + } + + if (json.getExt() != null) { + if (comma) { + sb.append(", "); + } + comma = true; + sb.append("\"ext\": ").append(json.getExt().toString()); + } + return sb.append("}").toString(); + } + }; + + @Test + public void testSimpleSerializeParse() throws ParseException { + String signer = "alice.pub"; + Verification.JSON json = new Verification.JSON(signer); + + String string = dummySerializer.serialize(json); + assertEquals("{\"signers\": [\"alice.pub\"]}", string); + + Verification.JSON parsed = dummyParser.parse(string); + assertEquals(signer, parsed.getSigners().get(0)); + assertEquals(1, parsed.getSigners().size()); + assertNull(parsed.getComment()); + assertNull(parsed.getExt()); + } + + @Test + public void testAdvancedSerializeParse() throws ParseException { + Verification.JSON json = new Verification.JSON( + Arrays.asList("../certs/alice.pub", "/etc/pgp/certs.pgp"), + "This is a comment", + "{\"Foo\": \"Bar\"}"); + + String serialized = dummySerializer.serialize(json); + assertEquals("{\"signers\": [\"../certs/alice.pub\", \"/etc/pgp/certs.pgp\"], \"comment\": \"This is a comment\", \"ext\": {\"Foo\": \"Bar\"}}", + serialized); + + Verification.JSON parsed = dummyParser.parse(serialized); + assertEquals(json.getSigners(), parsed.getSigners()); + assertEquals(json.getComment(), parsed.getComment()); + assertEquals(json.getExt(), parsed.getExt()); + } + + @Test + public void testVerificationWithSimpleJson() { + String string = "2019-10-29T18:36:45Z EB85BB5FA33A75E15E944E63F231550C4F47E38E EB85BB5FA33A75E15E944E63F231550C4F47E38E mode:text {\"signers\": [\"alice.pgp\"]}"; + Verification verification = Verification.fromString(string); + + assertTrue(verification.getContainsJson()); + assertEquals("EB85BB5FA33A75E15E944E63F231550C4F47E38E", verification.getSigningKeyFingerprint()); + assertEquals("EB85BB5FA33A75E15E944E63F231550C4F47E38E", verification.getSigningCertFingerprint()); + assertEquals(SignatureMode.text, verification.getSignatureMode().get()); + + Verification.JSON json = verification.getJson(dummyParser); + assertNotNull(json, "The verification string MUST contain valid extension json"); + + assertEquals(Collections.singletonList("alice.pgp"), json.getSigners()); + assertNull(json.getComment()); + assertNull(json.getExt()); + + verification = new Verification(verification.getCreationTime(), verification.getSigningKeyFingerprint(), verification.getSigningCertFingerprint(), verification.getSignatureMode().get(), json, dummySerializer); + assertEquals(string, verification.toString()); + } +} diff --git a/sop-java/src/test/java/sop/VerificationTest.java b/sop-java/src/test/java/sop/VerificationTest.java index e956435..1e10f61 100644 --- a/sop-java/src/test/java/sop/VerificationTest.java +++ b/sop-java/src/test/java/sop/VerificationTest.java @@ -13,6 +13,7 @@ import java.text.ParseException; import java.util.Date; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; public class VerificationTest { @@ -25,6 +26,8 @@ public class VerificationTest { Verification verification = new Verification(signDate, keyFP, certFP); assertEquals("2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B", verification.toString()); + assertFalse(verification.getContainsJson()); + VerificationAssert.assertThatVerification(verification) .issuedBy(certFP) .isBySigningKey(keyFP) From 21766a1f39f5bfc56553d930047fad607f7c29dd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 3 Jun 2025 23:18:34 +0200 Subject: [PATCH 280/298] Delete ProxyOutputStream and test --- .../main/kotlin/sop/util/ProxyOutputStream.kt | 77 ------------------- .../java/sop/util/ProxyOutputStreamTest.java | 40 ---------- 2 files changed, 117 deletions(-) delete mode 100644 sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt delete mode 100644 sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java diff --git a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt deleted file mode 100644 index 4ba24b8..0000000 --- a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util - -import java.io.ByteArrayOutputStream -import java.io.IOException -import java.io.OutputStream - -/** - * [OutputStream] that buffers data being written into it, until its underlying output stream is - * being replaced. At that point, first all the buffered data is being written to the underlying - * stream, followed by any successive data that may get written to the [ProxyOutputStream]. This - * 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.") -// TODO: Remove in 11.X -class ProxyOutputStream : OutputStream() { - private val buffer = ByteArrayOutputStream() - private var swapped: OutputStream? = null - - @Synchronized - fun replaceOutputStream(underlying: OutputStream) { - this.swapped = underlying - swapped!!.write(buffer.toByteArray()) - } - - @Synchronized - @Throws(IOException::class) - override fun write(b: ByteArray) { - if (swapped == null) { - buffer.write(b) - } else { - swapped!!.write(b) - } - } - - @Synchronized - @Throws(IOException::class) - override fun write(b: ByteArray, off: Int, len: Int) { - if (swapped == null) { - buffer.write(b, off, len) - } else { - swapped!!.write(b, off, len) - } - } - - @Synchronized - @Throws(IOException::class) - override fun flush() { - buffer.flush() - if (swapped != null) { - swapped!!.flush() - } - } - - @Synchronized - @Throws(IOException::class) - override fun close() { - buffer.close() - if (swapped != null) { - swapped!!.close() - } - } - - @Synchronized - @Throws(IOException::class) - override fun write(i: Int) { - if (swapped == null) { - buffer.write(i) - } else { - swapped!!.write(i) - } - } -} diff --git a/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java b/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java deleted file mode 100644 index 9d99fd4..0000000 --- a/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -import org.junit.jupiter.api.Test; - -public class ProxyOutputStreamTest { - - @Test - public void replaceOutputStreamThrowsNPEForNull() { - ProxyOutputStream proxy = new ProxyOutputStream(); - assertThrows(NullPointerException.class, () -> proxy.replaceOutputStream(null)); - } - - @Test - public void testSwappingStreamPreservesWrittenBytes() throws IOException { - byte[] firstSection = "Foo\nBar\n".getBytes(StandardCharsets.UTF_8); - byte[] secondSection = "Baz\n".getBytes(StandardCharsets.UTF_8); - - ProxyOutputStream proxy = new ProxyOutputStream(); - proxy.write(firstSection); - - ByteArrayOutputStream swappedStream = new ByteArrayOutputStream(); - proxy.replaceOutputStream(swappedStream); - - proxy.write(secondSection); - proxy.close(); - - assertEquals("Foo\nBar\nBaz\n", swappedStream.toString()); - } -} From 8a7fd5cb58ee1689ed7be2a00f9e08a55cf09985 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 11:15:29 +0200 Subject: [PATCH 281/298] Move validate-userid to SOPV --- external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt | 4 ++++ sop-java/src/main/kotlin/sop/SOP.kt | 3 --- sop-java/src/main/kotlin/sop/SOPV.kt | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt index f22f947..3341055 100644 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt @@ -10,9 +10,11 @@ import sop.SOPV import sop.external.ExternalSOP.TempDirProvider import sop.external.operation.DetachedVerifyExternal import sop.external.operation.InlineVerifyExternal +import sop.external.operation.ValidateUserIdExternal import sop.external.operation.VersionExternal import sop.operation.DetachedVerify import sop.operation.InlineVerify +import sop.operation.ValidateUserId import sop.operation.Version /** @@ -37,6 +39,8 @@ class ExternalSOPV( override fun inlineVerify(): InlineVerify = InlineVerifyExternal(binaryName, properties, tempDirProvider) + override fun validateUserId(): ValidateUserId = ValidateUserIdExternal(binaryName, properties) + companion object { /** diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index fbd0428..a942c56 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -68,7 +68,4 @@ interface SOP : SOPV { /** 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/SOPV.kt b/sop-java/src/main/kotlin/sop/SOPV.kt index 58a7f13..27eb6e3 100644 --- a/sop-java/src/main/kotlin/sop/SOPV.kt +++ b/sop-java/src/main/kotlin/sop/SOPV.kt @@ -6,6 +6,7 @@ package sop import sop.operation.DetachedVerify import sop.operation.InlineVerify +import sop.operation.ValidateUserId import sop.operation.Version /** Subset of [SOP] implementing only OpenPGP signature verification. */ @@ -31,4 +32,7 @@ interface SOPV { * a message, use [detachedVerify] instead. */ fun inlineVerify(): InlineVerify? + + /** Validate a UserID in an OpenPGP certificate. */ + fun validateUserId(): ValidateUserId? } From 2a22cea29b52da0abc238a5860c3e251380d3dc2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 11:28:28 +0200 Subject: [PATCH 282/298] Remove animalsniffer --- build.gradle | 13 ------------- version.gradle | 1 - 2 files changed, 14 deletions(-) diff --git a/build.gradle b/build.gradle index 1bff704..10f2b87 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,6 @@ buildscript { } plugins { - id 'ru.vyarus.animalsniffer' version '2.0.0' id 'org.jetbrains.kotlin.jvm' version "1.9.21" id 'com.diffplug.spotless' version '6.22.0' apply false } @@ -35,18 +34,6 @@ allprojects { apply plugin: 'kotlin-kapt' apply plugin: 'com.diffplug.spotless' - // For non-cli modules enable android api compatibility check - if (it.name.equals('sop-java')) { - // animalsniffer - apply plugin: 'ru.vyarus.animalsniffer' - dependencies { - signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:2.3.3_r2@signature" - } - animalsniffer { - sourceSets = [sourceSets.main] - } - } - // Only generate jar for submodules // https://stackoverflow.com/a/25445035 jar { diff --git a/version.gradle b/version.gradle index c757e1e..4d90e55 100644 --- a/version.gradle +++ b/version.gradle @@ -6,7 +6,6 @@ allprojects { ext { shortVersion = '11.0.0' isSnapshot = true - minAndroidSdk = 10 javaSourceCompatibility = 11 gsonVersion = '2.10.1' jsrVersion = '3.0.2' From ac17000ff16dc14bdf19768685fa27dbb27f52f4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 11:28:58 +0200 Subject: [PATCH 283/298] Clean up unused version literal --- version.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/version.gradle b/version.gradle index 4d90e55..a58af0b 100644 --- a/version.gradle +++ b/version.gradle @@ -10,7 +10,6 @@ allprojects { gsonVersion = '2.10.1' jsrVersion = '3.0.2' junitVersion = '5.8.2' - junitSysExitVersion = '1.1.2' logbackVersion = '1.2.13' // 1.4+ cause CLI spam mockitoVersion = '4.5.1' picocliVersion = '4.6.3' From e72e5a15c0c0a823072ca22f5a4fc57557a83672 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 11:29:25 +0200 Subject: [PATCH 284/298] Fix: Pass chars to StringBuilder.append() --- sop-java/src/test/java/sop/VerificationJSONTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sop-java/src/test/java/sop/VerificationJSONTest.java b/sop-java/src/test/java/sop/VerificationJSONTest.java index 10253d8..a80e6fc 100644 --- a/sop-java/src/test/java/sop/VerificationJSONTest.java +++ b/sop-java/src/test/java/sop/VerificationJSONTest.java @@ -81,12 +81,12 @@ public class VerificationJSONTest { sb.append("\"signers\": ["); for (Iterator iterator = json.getSigners().iterator(); iterator.hasNext(); ) { String signer = iterator.next(); - sb.append("\"").append(signer).append("\""); + sb.append('\"').append(signer).append('\"'); if (iterator.hasNext()) { sb.append(", "); } } - sb.append("]"); + sb.append(']'); } if (json.getComment() != null) { @@ -94,7 +94,7 @@ public class VerificationJSONTest { sb.append(", "); } comma = true; - sb.append("\"comment\": \"").append(json.getComment()).append("\""); + sb.append("\"comment\": \"").append(json.getComment()).append('\"'); } if (json.getExt() != null) { @@ -104,7 +104,7 @@ public class VerificationJSONTest { comma = true; sb.append("\"ext\": ").append(json.getExt().toString()); } - return sb.append("}").toString(); + return sb.append('}').toString(); } }; From 86718a26905c16af996dd5d4a15530eac4063ced Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 11:29:53 +0200 Subject: [PATCH 285/298] Bump logback to 1.5.13 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index a58af0b..864c585 100644 --- a/version.gradle +++ b/version.gradle @@ -10,7 +10,7 @@ allprojects { gsonVersion = '2.10.1' jsrVersion = '3.0.2' junitVersion = '5.8.2' - logbackVersion = '1.2.13' // 1.4+ cause CLI spam + logbackVersion = '1.5.13' mockitoVersion = '4.5.1' picocliVersion = '4.6.3' slf4jVersion = '1.7.36' From 01be696f75c3a48367b4e464a24238dcfa6e6ffd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 11:30:21 +0200 Subject: [PATCH 286/298] Bump version to 14.0.0-SNAPSHOT --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 864c585..cfb972a 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '11.0.0' + shortVersion = '14.0.0' isSnapshot = true javaSourceCompatibility = 11 gsonVersion = '2.10.1' From 04d154f63d5e9e797e5dec8b2a80fdfae734e7b8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 12:12:23 +0200 Subject: [PATCH 287/298] Version: Fix getSopJavaVersion() --- .../src/main/java/sop/testsuite/operation/VersionTest.java | 6 ++++++ sop-java/src/main/kotlin/sop/operation/Version.kt | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java index f836935..47644bf 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java @@ -86,4 +86,10 @@ public class VersionTest extends AbstractSOPTest { throw new TestAbortedException("Implementation does not provide coverage for any sopv interface version."); } } + + @ParameterizedTest + @MethodSource("provideInstances") + public void sopJavaVersionTest(SOP sop) { + assertNotNull(sop.version().getSopJavaVersion()); + } } diff --git a/sop-java/src/main/kotlin/sop/operation/Version.kt b/sop-java/src/main/kotlin/sop/operation/Version.kt index a10fe7c..6c8aa95 100644 --- a/sop-java/src/main/kotlin/sop/operation/Version.kt +++ b/sop-java/src/main/kotlin/sop/operation/Version.kt @@ -115,12 +115,12 @@ interface Version { fun getSopJavaVersion(): String? { return try { val resourceIn: InputStream = - javaClass.getResourceAsStream("/sop-java-version.properties") + Version::class.java.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 + "DEVELOPMENT" } } } From 12835bfb8e17538f1ea87f3d8ee3e1abb77d73e9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 12:12:41 +0200 Subject: [PATCH 288/298] Add/fix missing localizations for new SOP commands --- sop-java-picocli/src/main/resources/msg_update-key.properties | 2 +- .../src/main/resources/msg_update-key_de.properties | 2 +- sop-java-picocli/src/main/resources/msg_version.properties | 1 + sop-java-picocli/src/main/resources/msg_version_de.properties | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) 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 e12fbbc..0b5243e 100644 --- a/sop-java-picocli/src/main/resources/msg_update-key.properties +++ b/sop-java-picocli/src/main/resources/msg_update-key.properties @@ -4,7 +4,7 @@ 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 +no-added-capabilities=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 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 1b8a84d..91e5532 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 @@ -4,7 +4,7 @@ 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 +no-added-capabilities=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 diff --git a/sop-java-picocli/src/main/resources/msg_version.properties b/sop-java-picocli/src/main/resources/msg_version.properties index c7d0168..1327a78 100644 --- a/sop-java-picocli/src/main/resources/msg_version.properties +++ b/sop-java-picocli/src/main/resources/msg_version.properties @@ -5,6 +5,7 @@ usage.header=Display version information about the tool 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 +sopv=Print the SOPV API version standardOutput=version information 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 c317916..c99045c 100644 --- a/sop-java-picocli/src/main/resources/msg_version_de.properties +++ b/sop-java-picocli/src/main/resources/msg_version_de.properties @@ -5,6 +5,7 @@ usage.header=Zeige Versionsinformationen 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 +sopv=Gebe die SOPV API Version aus standardOutput=Versionsinformationen From 07d0aa69416c65b53fb6cf8d9bbfddd7525e8395 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 12:24:15 +0200 Subject: [PATCH 289/298] Update CHANGELOG --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0500cb..9271f3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 14.0.0-SNAPSHOT +- Implement changes from SOP spec `11`, `12`, `13`, `14` + - Implement `update-key` command + - Implement `merge-certs` command + - Implement `certify-userid` command + - Implement `validate-userid` command + - Add `UnspecificFailure` exception + - Add `KeyCannotCertify` exception + - Add `NoHardwareKeyFound` exception + - Add `HardwareKeyFailure` exception + - Add `PrimaryKeyBad` exception + - Add `CertUserIdNoMatch` exception + - `Verification`: Add support for JSON description extensions +- Remove `animalsniffer` from build dependencies +- Bump `logback` to `1.5.13` + ## 10.1.1 - Prepare jar files for use in native images, e.g. using GraalVM by generating and including configuration files for reflection, resources and dynamic proxies. From 191ec8c07d8cfe7bf3c18e887379d40baae65f82 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 13:22:16 +0200 Subject: [PATCH 290/298] Improve CHANGELOG again --- CHANGELOG.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9271f3d..3f32b28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,17 +7,20 @@ SPDX-License-Identifier: Apache-2.0 # Changelog ## 14.0.0-SNAPSHOT -- Implement changes from SOP spec `11`, `12`, `13`, `14` - - Implement `update-key` command - - Implement `merge-certs` command - - Implement `certify-userid` command - - Implement `validate-userid` command - - Add `UnspecificFailure` exception - - Add `KeyCannotCertify` exception - - Add `NoHardwareKeyFound` exception - - Add `HardwareKeyFailure` exception - - Add `PrimaryKeyBad` exception - - Add `CertUserIdNoMatch` exception +- Update implementation to [SOP Specification revision 14](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-14.html), + including changes from revisions `11`, `12`, `13`, `14`. + - Implement newly introduced operations + - `update-key` 'fixes' everything wrong with a key + - `merge-certs` merges a certificate with other copies + - `certify-userid` create signatures over user-ids on certificates + - `validate-userid` validate signatures over user-ids + - Add new exceptions + - `UnspecificFailure` maps generic application errors + - `KeyCannotCertify` signals that a key cannot be used for third-party certifications + - `NoHardwareKeyFound` signals that a key backed by a hardware device cannot be found + - `HardwareKeyFailure` signals a hardware device failure + - `PrimaryKeyBad` signals an unusable or bad primary key + - `CertUserIdNoMatch` signals that a user-id cannot be found/validated on a certificate - `Verification`: Add support for JSON description extensions - Remove `animalsniffer` from build dependencies - Bump `logback` to `1.5.13` From 9762f1f043546a260f901e16f30b4d0daaa243fb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 13:39:23 +0200 Subject: [PATCH 291/298] SOP-Java 14.0.0 --- CHANGELOG.md | 2 +- README.md | 2 +- version.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f32b28..0447a9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 14.0.0-SNAPSHOT +## 14.0.0 - Update implementation to [SOP Specification revision 14](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-14.html), including changes from revisions `11`, `12`, `13`, `14`. - Implement newly introduced operations diff --git a/README.md b/README.md index f2e207f..35324c4 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: 10](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/10/) +[![Spec Revision: 14](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/14/) [![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) diff --git a/version.gradle b/version.gradle index cfb972a..cb9b332 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '14.0.0' - isSnapshot = true + isSnapshot = false javaSourceCompatibility = 11 gsonVersion = '2.10.1' jsrVersion = '3.0.2' From b3223372c690aa7f8277ce226c34e6ad314d7d68 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 13:42:42 +0200 Subject: [PATCH 292/298] SOP-Java 14.0.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index cb9b332..bac96da 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '14.0.0' - isSnapshot = false + shortVersion = '14.0.1' + isSnapshot = true javaSourceCompatibility = 11 gsonVersion = '2.10.1' jsrVersion = '3.0.2' From c651adc0b391e764161f09e0eb0869985d468058 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 1 Jul 2025 14:38:13 +0200 Subject: [PATCH 293/298] Add VerificationAssert methods, disable tests for unsupported operations --- .../assertions/VerificationAssert.java | 25 ++++++ .../testsuite/operation/AbstractSOPTest.java | 16 ++++ .../testsuite/operation/ArmorDearmorTest.java | 40 ++++----- .../operation/CertifyValidateUserIdTest.java | 58 ++++++------- .../operation/ChangeKeyPasswordTest.java | 36 ++++---- .../operation/DecryptWithSessionKeyTest.java | 4 +- .../DetachedSignDetachedVerifyTest.java | 48 +++++------ .../operation/EncryptDecryptTest.java | 83 ++++++++++--------- .../testsuite/operation/ExtractCertTest.java | 22 ++--- .../testsuite/operation/GenerateKeyTest.java | 22 ++--- ...ineSignInlineDetachDetachedVerifyTest.java | 14 ++-- .../operation/InlineSignInlineVerifyTest.java | 33 ++++---- .../testsuite/operation/ListProfilesTest.java | 9 +- .../testsuite/operation/MergeCertsTest.java | 44 +++++----- .../testsuite/operation/RevokeKeyTest.java | 46 +++++----- .../sop/testsuite/operation/VersionTest.java | 22 ++--- .../test/java/sop/VerificationJSONTest.java | 3 + 17 files changed, 286 insertions(+), 239 deletions(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java index 5267148..dea8717 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java @@ -8,9 +8,13 @@ import sop.Verification; import sop.enums.SignatureMode; import sop.testsuite.JUtils; +import java.text.ParseException; import java.util.Date; +import java.util.function.Predicate; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; public final class VerificationAssert { @@ -57,6 +61,27 @@ public final class VerificationAssert { return hasDescription(description); } + public VerificationAssert hasValidJSONOrNull(Verification.JSONParser parser) + throws ParseException { + if (!verification.getJsonOrDescription().isPresent()) { + // missing description + return this; + } + + return hasJSON(parser, null); + } + + public VerificationAssert hasJSON(Verification.JSONParser parser, Predicate predicate) { + assertTrue(verification.getContainsJson(), "Verification does not appear to contain JSON extension"); + + Verification.JSON json = verification.getJson(parser); + assertNotNull(verification.getJson(parser), "Verification does not appear to contain valid JSON extension."); + if (predicate != null) { + assertTrue(predicate.test(json), "JSON object does not match predicate."); + } + return this; + } + public VerificationAssert hasMode(SignatureMode mode) { assertEquals(mode, verification.getSignatureMode().get()); return this; diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java index 6c163f7..16ae256 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java @@ -4,10 +4,13 @@ package sop.testsuite.operation; +import kotlin.jvm.functions.Function0; +import org.junit.jupiter.api.Assumptions; 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.exception.SOPGPException; import sop.testsuite.AbortOnUnsupportedOption; import sop.testsuite.AbortOnUnsupportedOptionExtension; import sop.testsuite.SOPInstanceFactory; @@ -18,6 +21,8 @@ import java.util.List; import java.util.Map; import java.util.stream.Stream; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + @ExtendWith(AbortOnUnsupportedOptionExtension.class) @AbortOnUnsupportedOption public abstract class AbstractSOPTest { @@ -51,6 +56,17 @@ public abstract class AbstractSOPTest { } } + public T assumeSupported(Function0 f) { + try { + T t = f.invoke(); + assumeTrue(t != null, "Unsupported operation."); + return t; + } catch (SOPGPException.UnsupportedSubcommand e) { + assumeTrue(false, e.getMessage()); + return null; + } + } + public static Stream provideBackends() { return backends.stream(); } diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java index 35959b0..00488e1 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java @@ -20,7 +20,7 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertArrayEquals; @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ArmorDearmorTest { +public class ArmorDearmorTest extends AbstractSOPTest { static Stream provideInstances() { return AbstractSOPTest.provideBackends(); @@ -31,13 +31,13 @@ public class ArmorDearmorTest { public void dearmorArmorAliceKey(SOP sop) throws IOException { byte[] aliceKey = TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(aliceKey) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -52,13 +52,13 @@ public class ArmorDearmorTest { public void dearmorArmorAliceCert(SOP sop) throws IOException { byte[] aliceCert = TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(aliceCert) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -73,13 +73,13 @@ public class ArmorDearmorTest { public void dearmorArmorBobKey(SOP sop) throws IOException { byte[] bobKey = TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(bobKey) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -94,13 +94,13 @@ public class ArmorDearmorTest { public void dearmorArmorBobCert(SOP sop) throws IOException { byte[] bobCert = TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(bobCert) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -115,13 +115,13 @@ public class ArmorDearmorTest { public void dearmorArmorCarolKey(SOP sop) throws IOException { byte[] carolKey = TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(carolKey) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -136,13 +136,13 @@ public class ArmorDearmorTest { public void dearmorArmorCarolCert(SOP sop) throws IOException { byte[] carolCert = TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(carolCert) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -163,13 +163,13 @@ public class ArmorDearmorTest { "CePQFpprprnGEzpE3flQLUc=\n" + "=ZiFR\n" + "-----END PGP MESSAGE-----\n").getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(message) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_MESSAGE)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -191,13 +191,13 @@ public class ArmorDearmorTest { "=GHvQ\n" + "-----END PGP SIGNATURE-----\n").getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(signature) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_SIGNATURE)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -210,11 +210,11 @@ public class ArmorDearmorTest { @ParameterizedTest @MethodSource("provideInstances") public void testDearmoringTwiceIsIdempotent(SOP sop) throws IOException { - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .getBytes(); - byte[] dearmoredAgain = sop.dearmor() + byte[] dearmoredAgain = assumeSupported(sop::dearmor) .data(dearmored) .getBytes(); @@ -233,7 +233,7 @@ public class ArmorDearmorTest { "=GHvQ\n" + "-----END PGP SIGNATURE-----\n").getBytes(StandardCharsets.UTF_8); - byte[] armoredAgain = sop.armor() + byte[] armoredAgain = assumeSupported(sop::armor) .data(armored) .getBytes(); diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java index 7f9f088..855c23d 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java @@ -18,7 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class CertifyValidateUserIdTest { +public class CertifyValidateUserIdTest extends AbstractSOPTest { static Stream provideInstances() { return AbstractSOPTest.provideBackends(); @@ -27,25 +27,25 @@ public class CertifyValidateUserIdTest { @ParameterizedTest @MethodSource("provideInstances") public void certifyUserId(SOP sop) throws IOException { - byte[] aliceKey = sop.generateKey() + byte[] aliceKey = assumeSupported(sop::generateKey) .withKeyPassword("sw0rdf1sh") .userId("Alice ") .generate() .getBytes(); - byte[] aliceCert = sop.extractCert() + byte[] aliceCert = assumeSupported(sop::extractCert) .key(aliceKey) .getBytes(); - byte[] bobKey = sop.generateKey() + byte[] bobKey = assumeSupported(sop::generateKey) .userId("Bob ") .generate() .getBytes(); - byte[] bobCert = sop.extractCert() + byte[] bobCert = assumeSupported(sop::extractCert) .key(bobKey) .getBytes(); // Alice has her own user-id self-certified - assertTrue(sop.validateUserId() + assertTrue(assumeSupported(sop::validateUserId) .authorities(aliceCert) .userId("Alice ") .subjects(aliceCert), @@ -53,20 +53,20 @@ public class CertifyValidateUserIdTest { // Alice has not yet certified Bobs user-id assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> - sop.validateUserId() + assumeSupported(sop::validateUserId) .authorities(aliceCert) .userId("Bob ") .subjects(bobCert), "Alice has not yet certified Bobs user-id"); - byte[] bobCertifiedByAlice = sop.certifyUserId() + byte[] bobCertifiedByAlice = assumeSupported(sop::certifyUserId) .userId("Bob ") .withKeyPassword("sw0rdf1sh") .keys(aliceKey) .certs(bobCert) .getBytes(); - assertTrue(sop.validateUserId() + assertTrue(assumeSupported(sop::validateUserId) .userId("Bob ") .authorities(aliceCert) .subjects(bobCertifiedByAlice), @@ -76,28 +76,28 @@ public class CertifyValidateUserIdTest { @ParameterizedTest @MethodSource("provideInstances") public void certifyUserIdUnarmored(SOP sop) throws IOException { - byte[] aliceKey = sop.generateKey() + byte[] aliceKey = assumeSupported(sop::generateKey) .noArmor() .withKeyPassword("sw0rdf1sh") .userId("Alice ") .generate() .getBytes(); - byte[] aliceCert = sop.extractCert() + byte[] aliceCert = assumeSupported(sop::extractCert) .noArmor() .key(aliceKey) .getBytes(); - byte[] bobKey = sop.generateKey() + byte[] bobKey = assumeSupported(sop::generateKey) .noArmor() .userId("Bob ") .generate() .getBytes(); - byte[] bobCert = sop.extractCert() + byte[] bobCert = assumeSupported(sop::extractCert) .noArmor() .key(bobKey) .getBytes(); - byte[] bobCertifiedByAlice = sop.certifyUserId() + byte[] bobCertifiedByAlice = assumeSupported(sop::certifyUserId) .noArmor() .userId("Bob ") .withKeyPassword("sw0rdf1sh") @@ -105,7 +105,7 @@ public class CertifyValidateUserIdTest { .certs(bobCert) .getBytes(); - assertTrue(sop.validateUserId() + assertTrue(assumeSupported(sop::validateUserId) .userId("Bob ") .authorities(aliceCert) .subjects(bobCertifiedByAlice), @@ -115,45 +115,45 @@ public class CertifyValidateUserIdTest { @ParameterizedTest @MethodSource("provideInstances") public void addPetName(SOP sop) throws IOException { - byte[] aliceKey = sop.generateKey() + byte[] aliceKey = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); - byte[] aliceCert = sop.extractCert() + byte[] aliceCert = assumeSupported(sop::extractCert) .key(aliceKey) .getBytes(); - byte[] bobKey = sop.generateKey() + byte[] bobKey = assumeSupported(sop::generateKey) .userId("Bob ") .generate() .getBytes(); - byte[] bobCert = sop.extractCert() + byte[] bobCert = assumeSupported(sop::extractCert) .key(bobKey) .getBytes(); assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> - sop.certifyUserId() + assumeSupported(sop::certifyUserId) .userId("Bobby") .keys(aliceKey) .certs(bobCert) .getBytes(), "Alice cannot create a pet-name for Bob without the --no-require-self-sig flag"); - byte[] bobWithPetName = sop.certifyUserId() + byte[] bobWithPetName = assumeSupported(sop::certifyUserId) .userId("Bobby") .noRequireSelfSig() .keys(aliceKey) .certs(bobCert) .getBytes(); - assertTrue(sop.validateUserId() + assertTrue(assumeSupported(sop::validateUserId) .userId("Bobby") .authorities(aliceCert) .subjects(bobWithPetName), "Alice accepts the pet-name she gave to Bob"); assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> - sop.validateUserId() + assumeSupported(sop::validateUserId) .userId("Bobby") .authorities(bobWithPetName) .subjects(bobWithPetName), @@ -163,28 +163,28 @@ public class CertifyValidateUserIdTest { @ParameterizedTest @MethodSource("provideInstances") public void certifyWithRevokedKey(SOP sop) throws IOException { - byte[] aliceKey = sop.generateKey() + byte[] aliceKey = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); - byte[] aliceRevokedCert = sop.revokeKey() + byte[] aliceRevokedCert = assumeSupported(sop::revokeKey) .keys(aliceKey) .getBytes(); - byte[] aliceRevokedKey = sop.updateKey() + byte[] aliceRevokedKey = assumeSupported(sop::updateKey) .mergeCerts(aliceRevokedCert) .key(aliceKey) .getBytes(); - byte[] bobKey = sop.generateKey() + byte[] bobKey = assumeSupported(sop::generateKey) .userId("Bob ") .generate() .getBytes(); - byte[] bobCert = sop.extractCert() + byte[] bobCert = assumeSupported(sop::extractCert) .key(bobKey) .getBytes(); assertThrows(SOPGPException.KeyCannotCertify.class, () -> - sop.certifyUserId() + assumeSupported(sop::certifyUserId) .userId("Bob ") .keys(aliceRevokedKey) .certs(bobCert) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java index 8948dda..a62cbb8 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java @@ -32,18 +32,18 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void changePasswordFromUnprotectedToProtected(SOP sop) throws IOException { - byte[] unprotectedKey = sop.generateKey().generate().getBytes(); + byte[] unprotectedKey = assumeSupported(sop::generateKey).generate().getBytes(); byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); - byte[] protectedKey = sop.changeKeyPassword().newKeyPassphrase(password).keys(unprotectedKey).getBytes(); + byte[] protectedKey = assumeSupported(sop::changeKeyPassword).newKeyPassphrase(password).keys(unprotectedKey).getBytes(); - sop.sign().withKeyPassword(password).key(protectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); + assumeSupported(sop::sign).withKeyPassword(password).key(protectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); } @ParameterizedTest @MethodSource("provideInstances") public void changePasswordFromUnprotectedToUnprotected(SOP sop) throws IOException { - byte[] unprotectedKey = sop.generateKey().noArmor().generate().getBytes(); - byte[] stillUnprotectedKey = sop.changeKeyPassword().noArmor().keys(unprotectedKey).getBytes(); + byte[] unprotectedKey = assumeSupported(sop::generateKey).noArmor().generate().getBytes(); + byte[] stillUnprotectedKey = assumeSupported(sop::changeKeyPassword).noArmor().keys(unprotectedKey).getBytes(); assertArrayEquals(unprotectedKey, stillUnprotectedKey); } @@ -52,12 +52,12 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { @MethodSource("provideInstances") public void changePasswordFromProtectedToUnprotected(SOP sop) throws IOException { byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); - byte[] protectedKey = sop.generateKey().withKeyPassword(password).generate().getBytes(); - byte[] unprotectedKey = sop.changeKeyPassword() + byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(password).generate().getBytes(); + byte[] unprotectedKey = assumeSupported(sop::changeKeyPassword) .oldKeyPassphrase(password) .keys(protectedKey).getBytes(); - sop.sign().key(unprotectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); + assumeSupported(sop::sign).key(unprotectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); } @ParameterizedTest @@ -65,13 +65,13 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { public void changePasswordFromProtectedToDifferentProtected(SOP sop) throws IOException { byte[] oldPassword = "sw0rdf1sh".getBytes(UTF8Util.UTF8); byte[] newPassword = "0r4ng3".getBytes(UTF8Util.UTF8); - byte[] protectedKey = sop.generateKey().withKeyPassword(oldPassword).generate().getBytes(); - byte[] reprotectedKey = sop.changeKeyPassword() + byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(oldPassword).generate().getBytes(); + byte[] reprotectedKey = assumeSupported(sop::changeKeyPassword) .oldKeyPassphrase(oldPassword) .newKeyPassphrase(newPassword) .keys(protectedKey).getBytes(); - sop.sign().key(reprotectedKey).withKeyPassword(newPassword).data("Test123".getBytes(StandardCharsets.UTF_8)); + assumeSupported(sop::sign).key(reprotectedKey).withKeyPassword(newPassword).data("Test123".getBytes(StandardCharsets.UTF_8)); } @@ -82,8 +82,8 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { byte[] newPassword = "monkey123".getBytes(UTF8Util.UTF8); byte[] wrongPassword = "0r4ng3".getBytes(UTF8Util.UTF8); - byte[] protectedKey = sop.generateKey().withKeyPassword(oldPassword).generate().getBytes(); - assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.changeKeyPassword() + byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(oldPassword).generate().getBytes(); + assertThrows(SOPGPException.KeyIsProtected.class, () -> assumeSupported(sop::changeKeyPassword) .oldKeyPassphrase(wrongPassword) .newKeyPassphrase(newPassword) .keys(protectedKey).getBytes()); @@ -93,9 +93,9 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { @MethodSource("provideInstances") public void nonUtf8PasswordsFail(SOP sop) { assertThrows(SOPGPException.PasswordNotHumanReadable.class, () -> - sop.changeKeyPassword().oldKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); + assumeSupported(sop::changeKeyPassword).oldKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); assertThrows(SOPGPException.PasswordNotHumanReadable.class, () -> - sop.changeKeyPassword().newKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); + assumeSupported(sop::changeKeyPassword).newKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); } @@ -104,16 +104,16 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { public void testNoArmor(SOP sop) throws IOException { byte[] oldPassword = "sw0rdf1sh".getBytes(UTF8Util.UTF8); byte[] newPassword = "0r4ng3".getBytes(UTF8Util.UTF8); - byte[] protectedKey = sop.generateKey().withKeyPassword(oldPassword).generate().getBytes(); + byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(oldPassword).generate().getBytes(); - byte[] armored = sop.changeKeyPassword() + byte[] armored = assumeSupported(sop::changeKeyPassword) .oldKeyPassphrase(oldPassword) .newKeyPassphrase(newPassword) .keys(protectedKey) .getBytes(); JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); - byte[] unarmored = sop.changeKeyPassword() + byte[] unarmored = assumeSupported(sop::changeKeyPassword) .noArmor() .oldKeyPassphrase(oldPassword) .newKeyPassphrase(newPassword) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java index 65ec4a5..8fd201a 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java @@ -41,7 +41,7 @@ public class DecryptWithSessionKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testDecryptAndExtractSessionKey(SOP sop) throws IOException { - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .ciphertext(CIPHERTEXT.getBytes(StandardCharsets.UTF_8)) .toByteArrayAndResult(); @@ -54,7 +54,7 @@ public class DecryptWithSessionKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testDecryptWithSessionKey(SOP sop) throws IOException { - byte[] decrypted = sop.decrypt() + byte[] decrypted = assumeSupported(sop::decrypt) .withSessionKey(SessionKey.fromString(SESSION_KEY)) .ciphertext(CIPHERTEXT.getBytes(StandardCharsets.UTF_8)) .toByteArrayAndResult() diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java index e404599..415b9db 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java @@ -37,13 +37,13 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { public void signVerifyWithAliceKey(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signature = sop.detachedSign() + byte[] signature = assumeSupported(sop::detachedSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult() .getBytes(); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signature) .data(message); @@ -60,14 +60,14 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { public void signVerifyTextModeWithAliceKey(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signature = sop.detachedSign() + byte[] signature = assumeSupported(sop::detachedSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .mode(SignAs.text) .data(message) .toByteArrayAndResult() .getBytes(); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signature) .data(message); @@ -85,7 +85,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); byte[] signature = TestData.ALICE_DETACHED_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signature) .data(message); @@ -101,13 +101,13 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { public void signVerifyWithBobKey(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signature = sop.detachedSign() + byte[] signature = assumeSupported(sop::detachedSign) .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult() .getBytes(); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signature) .data(message); @@ -123,13 +123,13 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { public void signVerifyWithCarolKey(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signature = sop.detachedSign() + byte[] signature = assumeSupported(sop::detachedSign) .key(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult() .getBytes(); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signature) .data(message); @@ -145,7 +145,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { public void signVerifyWithEncryptedKey(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signature = sop.detachedSign() + byte[] signature = assumeSupported(sop::detachedSign) .key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8)) .withKeyPassword(TestData.PASSWORD) .data(message) @@ -154,7 +154,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { JUtils.assertArrayStartsWith(signature, TestData.BEGIN_PGP_SIGNATURE); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.PASSWORD_PROTECTED_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signature) .data(message); @@ -170,18 +170,18 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { public void signArmorVerifyWithBobKey(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signature = sop.detachedSign() + byte[] signature = assumeSupported(sop::detachedSign) .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .noArmor() .data(message) .toByteArrayAndResult() .getBytes(); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(signature) .getBytes(); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(armored) .data(message); @@ -199,7 +199,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { byte[] signature = TestData.ALICE_DETACHED_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8); Date beforeSignature = new Date(TestData.ALICE_DETACHED_SIGNED_MESSAGE_DATE.getTime() - 1000); // 1 sec before sig - assertThrows(SOPGPException.NoSignature.class, () -> sop.detachedVerify() + assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .notAfter(beforeSignature) .signatures(signature) @@ -213,7 +213,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { byte[] signature = TestData.ALICE_DETACHED_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8); Date afterSignature = new Date(TestData.ALICE_DETACHED_SIGNED_MESSAGE_DATE.getTime() + 1000); // 1 sec after sig - assertThrows(SOPGPException.NoSignature.class, () -> sop.detachedVerify() + assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .notBefore(afterSignature) .signatures(signature) @@ -224,13 +224,13 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { @MethodSource("provideInstances") public void signWithAliceVerifyWithBobThrowsNoSignature(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signatures = sop.detachedSign() + byte[] signatures = assumeSupported(sop::detachedSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult() .getBytes(); - assertThrows(SOPGPException.NoSignature.class, () -> sop.detachedVerify() + assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::detachedVerify) .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signatures) .data(message)); @@ -240,7 +240,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { @MethodSource("provideInstances") public void signVerifyWithEncryptedKeyWithoutPassphraseFails(SOP sop) { assertThrows(SOPGPException.KeyIsProtected.class, () -> - sop.detachedSign() + assumeSupported(sop::detachedSign) .key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8)) .data(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) .toByteArrayAndResult() @@ -253,7 +253,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signature = sop.sign() + byte[] signature = assumeSupported(sop::sign) .key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8)) .withKeyPassword("wrong") .withKeyPassword(TestData.PASSWORD) // correct @@ -262,7 +262,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { .toByteArrayAndResult() .getBytes(); - List verificationList = sop.verify() + List verificationList = assumeSupported(sop::verify) .cert(TestData.PASSWORD_PROTECTED_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signature) .data(message); @@ -279,7 +279,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); assertThrows(SOPGPException.MissingArg.class, () -> - sop.verify() + assumeSupported(sop::verify) .signatures(TestData.ALICE_DETACHED_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8)) .data(message)); } @@ -288,14 +288,14 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { @MethodSource("provideInstances") public void signVerifyWithMultipleKeys(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signatures = sop.detachedSign() + byte[] signatures = assumeSupported(sop::detachedSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult() .getBytes(); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signatures) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java index 330ca90..937b5b7 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java @@ -49,7 +49,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { @MethodSource("provideInstances") public void encryptDecryptRoundTripPasswordTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - ByteArrayAndResult encResult = sop.encrypt() + ByteArrayAndResult encResult = assumeSupported(sop::encrypt) .withPassword("sw0rdf1sh") .plaintext(message) .toByteArrayAndResult(); @@ -57,7 +57,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] ciphertext = encResult.getBytes(); Optional encSessionKey = encResult.getResult().getSessionKey(); - ByteArrayAndResult decResult = sop.decrypt() + ByteArrayAndResult decResult = assumeSupported(sop::decrypt) .withPassword("sw0rdf1sh") .ciphertext(ciphertext) .toByteArrayAndResult(); @@ -65,9 +65,10 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] plaintext = decResult.getBytes(); Optional decSessionKey = decResult.getResult().getSessionKey(); - assertArrayEquals(message, plaintext); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); if (encSessionKey.isPresent() && decSessionKey.isPresent()) { - assertEquals(encSessionKey.get(), decSessionKey.get()); + assertEquals(encSessionKey.get(), decSessionKey.get(), + "Extracted Session Key mismatch."); } } @@ -75,91 +76,93 @@ public class EncryptDecryptTest extends AbstractSOPTest { @MethodSource("provideInstances") public void encryptDecryptRoundTripAliceTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + byte[] ciphertext = assumeSupported(sop::encrypt) .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .plaintext(message) .toByteArrayAndResult() .getBytes(); - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .ciphertext(ciphertext) .toByteArrayAndResult(); byte[] plaintext = bytesAndResult.getBytes(); - assertArrayEquals(message, plaintext); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); DecryptionResult result = bytesAndResult.getResult(); - assertNotNull(result.getSessionKey().get()); + if (result.getSessionKey().isPresent()) { + assertNotNull(result.getSessionKey().get(), "Session key MUST NOT be null."); + } } @ParameterizedTest @MethodSource("provideInstances") public void encryptDecryptRoundTripBobTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + byte[] ciphertext = assumeSupported(sop::encrypt) .withCert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .plaintext(message) .toByteArrayAndResult() .getBytes(); - byte[] plaintext = sop.decrypt() + byte[] plaintext = assumeSupported(sop::decrypt) .withKey(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .ciphertext(ciphertext) .toByteArrayAndResult() .getBytes(); - assertArrayEquals(message, plaintext); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); } @ParameterizedTest @MethodSource("provideInstances") public void encryptDecryptRoundTripCarolTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + byte[] ciphertext = assumeSupported(sop::encrypt) .withCert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8)) .plaintext(message) .toByteArrayAndResult() .getBytes(); - byte[] plaintext = sop.decrypt() + byte[] plaintext = assumeSupported(sop::decrypt) .withKey(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8)) .ciphertext(ciphertext) .toByteArrayAndResult() .getBytes(); - assertArrayEquals(message, plaintext); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); } @ParameterizedTest @MethodSource("provideInstances") public void encryptNoArmorThenArmorThenDecryptRoundTrip(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + byte[] ciphertext = assumeSupported(sop::encrypt) .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .noArmor() .plaintext(message) .toByteArrayAndResult() .getBytes(); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(ciphertext) .getBytes(); - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .ciphertext(armored) .toByteArrayAndResult(); byte[] plaintext = bytesAndResult.getBytes(); - assertArrayEquals(message, plaintext); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); } @ParameterizedTest @MethodSource("provideInstances") public void encryptSignDecryptVerifyRoundTripAliceTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + byte[] ciphertext = assumeSupported(sop::encrypt) .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .mode(EncryptAs.binary) @@ -167,17 +170,19 @@ public class EncryptDecryptTest extends AbstractSOPTest { .toByteArrayAndResult() .getBytes(); - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .ciphertext(ciphertext) .toByteArrayAndResult(); byte[] plaintext = bytesAndResult.getBytes(); - assertArrayEquals(message, plaintext); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); DecryptionResult result = bytesAndResult.getResult(); - assertNotNull(result.getSessionKey().get()); + if (result.getSessionKey().isPresent()) { + assertNotNull(result.getSessionKey().get(), "Session key MUST NOT be null."); + } List verificationList = result.getVerifications(); VerificationListAssert.assertThatVerificationList(verificationList) @@ -191,7 +196,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { @MethodSource("provideInstances") public void encryptSignAsTextDecryptVerifyRoundTripAliceTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + byte[] ciphertext = assumeSupported(sop::encrypt) .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .mode(EncryptAs.text) @@ -199,14 +204,14 @@ public class EncryptDecryptTest extends AbstractSOPTest { .toByteArrayAndResult() .getBytes(); - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .ciphertext(ciphertext) .toByteArrayAndResult(); byte[] plaintext = bytesAndResult.getBytes(); - assertArrayEquals(message, plaintext); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); DecryptionResult result = bytesAndResult.getResult(); assertNotNull(result.getSessionKey().get()); @@ -222,17 +227,17 @@ public class EncryptDecryptTest extends AbstractSOPTest { @MethodSource("provideInstances") public void encryptSignDecryptVerifyRoundTripWithFreshEncryptedKeyTest(SOP sop) throws IOException { byte[] keyPassword = "sw0rdf1sh".getBytes(StandardCharsets.UTF_8); - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .withKeyPassword(keyPassword) .userId("Alice ") .generate() .getBytes(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .key(key) .getBytes(); byte[] message = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + byte[] ciphertext = assumeSupported(sop::encrypt) .withCert(cert) .signWith(key) .withKeyPassword(keyPassword) @@ -240,7 +245,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { .toByteArrayAndResult() .getBytes(); - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(key) .withKeyPassword(keyPassword) .verifyWithCert(cert) @@ -273,7 +278,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { Date beforeSignature = new Date(signatureDate.getTime() - 1000); // 1 sec before signing date assertThrows(SOPGPException.NoSignature.class, () -> { - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .verifyNotAfter(beforeSignature) @@ -307,7 +312,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { Date afterSignature = new Date(signatureDate.getTime() + 1000); // 1 sec after signing date assertThrows(SOPGPException.NoSignature.class, () -> { - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .verifyNotBefore(afterSignature) @@ -326,7 +331,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { public void missingArgsTest(SOP sop) { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - assertThrows(SOPGPException.MissingArg.class, () -> sop.encrypt() + assertThrows(SOPGPException.MissingArg.class, () -> assumeSupported(sop::encrypt) .plaintext(message) .toByteArrayAndResult() .getBytes()); @@ -336,7 +341,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { @MethodSource("provideInstances") public void passingSecretKeysForPublicKeysFails(SOP sop) { assertThrows(SOPGPException.BadData.class, () -> - sop.encrypt() + assumeSupported(sop::encrypt) .withCert(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) .toByteArrayAndResult() @@ -346,19 +351,19 @@ public class EncryptDecryptTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void encryptDecryptWithAllSupportedKeyGenerationProfiles(SOP sop) throws IOException { - List profiles = sop.listProfiles().generateKey(); + List profiles = assumeSupported(sop::listProfiles).generateKey(); List keys = new ArrayList<>(); List certs = new ArrayList<>(); for (Profile p : profiles) { - byte[] k = sop.generateKey() + byte[] k = assumeSupported(sop::generateKey) .profile(p) .userId(p.getName()) .generate() .getBytes(); keys.add(k); - byte[] c = sop.extractCert() + byte[] c = assumeSupported(sop::extractCert) .key(k) .getBytes(); certs.add(c); @@ -366,7 +371,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] plaintext = "Hello, World!\n".getBytes(); - Encrypt encrypt = sop.encrypt(); + Encrypt encrypt = assumeSupported(sop::encrypt); for (byte[] c : certs) { encrypt.withCert(c); } @@ -380,7 +385,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] ciphertext = encRes.getBytes(); for (byte[] k : keys) { - Decrypt decrypt = sop.decrypt() + Decrypt decrypt = assumeSupported(sop::decrypt) .withKey(k); for (byte[] c : certs) { decrypt.verifyWithCert(c); @@ -389,7 +394,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { .toByteArrayAndResult(); DecryptionResult dResult = decRes.getResult(); byte[] decPlaintext = decRes.getBytes(); - assertArrayEquals(plaintext, decPlaintext); + assertArrayEquals(plaintext, decPlaintext, "Decrypted plaintext does not match original plaintext."); assertEquals(certs.size(), dResult.getVerifications().size()); } } diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java index 99acf81..94d9927 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java @@ -28,12 +28,12 @@ public class ExtractCertTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void extractArmoredCertFromArmoredKeyTest(SOP sop) throws IOException { - InputStream keyIn = sop.generateKey() + InputStream keyIn = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getInputStream(); - byte[] cert = sop.extractCert().key(keyIn).getBytes(); + byte[] cert = assumeSupported(sop::extractCert).key(keyIn).getBytes(); JUtils.assertArrayStartsWith(cert, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK); JUtils.assertArrayEndsWithIgnoreNewlines(cert, TestData.END_PGP_PUBLIC_KEY_BLOCK); } @@ -41,7 +41,7 @@ public class ExtractCertTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void extractAliceCertFromAliceKeyTest(SOP sop) throws IOException { - byte[] armoredCert = sop.extractCert() + byte[] armoredCert = assumeSupported(sop::extractCert) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .getBytes(); JUtils.assertAsciiArmorEquals(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8), armoredCert); @@ -50,7 +50,7 @@ public class ExtractCertTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void extractBobsCertFromBobsKeyTest(SOP sop) throws IOException { - byte[] armoredCert = sop.extractCert() + byte[] armoredCert = assumeSupported(sop::extractCert) .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .getBytes(); JUtils.assertAsciiArmorEquals(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8), armoredCert); @@ -59,7 +59,7 @@ public class ExtractCertTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void extractCarolsCertFromCarolsKeyTest(SOP sop) throws IOException { - byte[] armoredCert = sop.extractCert() + byte[] armoredCert = assumeSupported(sop::extractCert) .key(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8)) .getBytes(); JUtils.assertAsciiArmorEquals(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8), armoredCert); @@ -68,12 +68,12 @@ public class ExtractCertTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void extractUnarmoredCertFromArmoredKeyTest(SOP sop) throws IOException { - InputStream keyIn = sop.generateKey() + InputStream keyIn = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getInputStream(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .noArmor() .key(keyIn) .getBytes(); @@ -84,13 +84,13 @@ public class ExtractCertTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void extractArmoredCertFromUnarmoredKeyTest(SOP sop) throws IOException { - InputStream keyIn = sop.generateKey() + InputStream keyIn = assumeSupported(sop::generateKey) .userId("Alice ") .noArmor() .generate() .getInputStream(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .key(keyIn) .getBytes(); @@ -101,13 +101,13 @@ public class ExtractCertTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void extractUnarmoredCertFromUnarmoredKeyTest(SOP sop) throws IOException { - InputStream keyIn = sop.generateKey() + InputStream keyIn = assumeSupported(sop::generateKey) .noArmor() .userId("Alice ") .generate() .getInputStream(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .noArmor() .key(keyIn) .getBytes(); diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java index 0d8b78e..787cf62 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java @@ -33,7 +33,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyTest(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); @@ -45,7 +45,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyNoArmor(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .noArmor() .generate() @@ -57,7 +57,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyWithMultipleUserIdsTest(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .userId("Bob ") .generate() @@ -70,7 +70,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyWithoutUserIdTest(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .generate() .getBytes(); @@ -81,7 +81,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyWithPasswordTest(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .withKeyPassword("sw0rdf1sh") .generate() @@ -94,7 +94,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyWithMultipleUserIdsAndPassword(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .userId("Bob ") .withKeyPassword("sw0rdf1sh") @@ -108,17 +108,17 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateSigningOnlyKey(SOP sop) throws IOException { - byte[] signingOnlyKey = sop.generateKey() + byte[] signingOnlyKey = assumeSupported(sop::generateKey) .signingOnly() .userId("Alice ") .generate() .getBytes(); - byte[] signingOnlyCert = sop.extractCert() + byte[] signingOnlyCert = assumeSupported(sop::extractCert) .key(signingOnlyKey) .getBytes(); assertThrows(SOPGPException.CertCannotEncrypt.class, () -> - sop.encrypt().withCert(signingOnlyCert) + assumeSupported(sop::encrypt).withCert(signingOnlyCert) .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) .toByteArrayAndResult() .getBytes()); @@ -127,7 +127,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyWithSupportedProfiles(SOP sop) throws IOException { - List profiles = sop.listProfiles() + List profiles = assumeSupported(sop::listProfiles) .generateKey(); for (Profile profile : profiles) { @@ -138,7 +138,7 @@ public class GenerateKeyTest extends AbstractSOPTest { private void generateKeyWithProfile(SOP sop, String profile) throws IOException { byte[] key; try { - key = sop.generateKey() + key = assumeSupported(sop::generateKey) .profile(profile) .userId("Alice ") .generate() diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java index 3e20a09..ac043b3 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java @@ -36,12 +36,12 @@ public class InlineSignInlineDetachDetachedVerifyTest extends AbstractSOPTest { public void inlineSignThenDetachThenDetachedVerifyTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = sop.inlineSign() + byte[] inlineSigned = assumeSupported(sop::inlineSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .getBytes(); - ByteArrayAndResult bytesAndResult = sop.inlineDetach() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::inlineDetach) .message(inlineSigned) .toByteArrayAndResult(); @@ -51,7 +51,7 @@ public class InlineSignInlineDetachDetachedVerifyTest extends AbstractSOPTest { byte[] signatures = bytesAndResult.getResult() .getBytes(); - List verifications = sop.detachedVerify() + List verifications = assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signatures) .data(plaintext); @@ -64,12 +64,12 @@ public class InlineSignInlineDetachDetachedVerifyTest extends AbstractSOPTest { public void inlineSignThenDetachNoArmorThenArmorThenDetachedVerifyTest(SOP sop) throws IOException { byte[] message = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = sop.inlineSign() + byte[] inlineSigned = assumeSupported(sop::inlineSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .getBytes(); - ByteArrayAndResult bytesAndResult = sop.inlineDetach() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::inlineDetach) .noArmor() .message(inlineSigned) .toByteArrayAndResult(); @@ -81,12 +81,12 @@ public class InlineSignInlineDetachDetachedVerifyTest extends AbstractSOPTest { .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(signatures, TestData.BEGIN_PGP_SIGNATURE)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(signatures) .getBytes(); JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_SIGNATURE); - List verifications = sop.detachedVerify() + List verifications = assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(armored) .data(plaintext); diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java index 39a26c6..d751ee8 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java @@ -40,14 +40,14 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { public void inlineSignVerifyAlice(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = sop.inlineSign() + byte[] inlineSigned = assumeSupported(sop::inlineSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .getBytes(); JUtils.assertArrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE); - ByteArrayAndResult> bytesAndResult = sop.inlineVerify() + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(inlineSigned) .toByteArrayAndResult(); @@ -66,7 +66,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { public void inlineSignVerifyAliceNoArmor(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = sop.inlineSign() + byte[] inlineSigned = assumeSupported(sop::inlineSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .noArmor() .data(message) @@ -74,7 +74,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { Assertions.assertFalse(JUtils.arrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE)); - ByteArrayAndResult> bytesAndResult = sop.inlineVerify() + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(inlineSigned) .toByteArrayAndResult(); @@ -93,7 +93,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { public void clearsignVerifyAlice(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] clearsigned = sop.inlineSign() + byte[] clearsigned = assumeSupported(sop::inlineSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .mode(InlineSignAs.clearsigned) .data(message) @@ -101,12 +101,13 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { JUtils.assertArrayStartsWith(clearsigned, TestData.BEGIN_PGP_SIGNED_MESSAGE); - ByteArrayAndResult> bytesAndResult = sop.inlineVerify() + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(clearsigned) .toByteArrayAndResult(); - assertArrayEquals(message, bytesAndResult.getBytes()); + assertArrayEquals(message, bytesAndResult.getBytes(), + "ASCII armored message does not appear to start with the 'BEGIN PGP SIGNED MESSAGE' header."); List verificationList = bytesAndResult.getResult(); VerificationListAssert.assertThatVerificationList(verificationList) @@ -121,7 +122,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { byte[] message = TestData.ALICE_INLINE_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8); Date signatureDate = TestData.ALICE_INLINE_SIGNED_MESSAGE_DATE; - ByteArrayAndResult> bytesAndResult = sop.inlineVerify() + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult(); @@ -141,7 +142,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { Date signatureDate = TestData.ALICE_INLINE_SIGNED_MESSAGE_DATE; Date afterSignature = new Date(signatureDate.getTime() + 1000); // 1 sec before sig - assertThrows(SOPGPException.NoSignature.class, () -> sop.inlineVerify() + assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::inlineVerify) .notBefore(afterSignature) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(message) @@ -155,7 +156,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { Date signatureDate = TestData.ALICE_INLINE_SIGNED_MESSAGE_DATE; Date beforeSignature = new Date(signatureDate.getTime() - 1000); // 1 sec before sig - assertThrows(SOPGPException.NoSignature.class, () -> sop.inlineVerify() + assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::inlineVerify) .notAfter(beforeSignature) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(message) @@ -167,14 +168,14 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { public void inlineSignVerifyBob(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = sop.inlineSign() + byte[] inlineSigned = assumeSupported(sop::inlineSign) .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .getBytes(); JUtils.assertArrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE); - ByteArrayAndResult> bytesAndResult = sop.inlineVerify() + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .data(inlineSigned) .toByteArrayAndResult(); @@ -193,14 +194,14 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { public void inlineSignVerifyCarol(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = sop.inlineSign() + byte[] inlineSigned = assumeSupported(sop::inlineSign) .key(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .getBytes(); JUtils.assertArrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE); - ByteArrayAndResult> bytesAndResult = sop.inlineVerify() + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) .cert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8)) .data(inlineSigned) .toByteArrayAndResult(); @@ -219,14 +220,14 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { public void inlineSignVerifyProtectedKey(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = sop.inlineSign() + byte[] inlineSigned = assumeSupported(sop::inlineSign) .withKeyPassword(TestData.PASSWORD) .key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8)) .mode(InlineSignAs.binary) .data(message) .getBytes(); - ByteArrayAndResult> bytesAndResult = sop.inlineVerify() + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) .cert(TestData.PASSWORD_PROTECTED_CERT.getBytes(StandardCharsets.UTF_8)) .data(inlineSigned) .toByteArrayAndResult(); diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java index 6d3c4c4..4faa1b3 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java @@ -26,8 +26,7 @@ public class ListProfilesTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void listGenerateKeyProfiles(SOP sop) { - List profiles = sop - .listProfiles() + List profiles = assumeSupported(sop::listProfiles) .generateKey(); assertFalse(profiles.isEmpty()); @@ -36,8 +35,7 @@ public class ListProfilesTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void listEncryptProfiles(SOP sop) { - List profiles = sop - .listProfiles() + List profiles = assumeSupported(sop::listProfiles) .encrypt(); assertFalse(profiles.isEmpty()); @@ -46,8 +44,7 @@ public class ListProfilesTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void listUnsupportedProfiles(SOP sop) { - assertThrows(SOPGPException.UnsupportedProfile.class, () -> sop - .listProfiles() + assertThrows(SOPGPException.UnsupportedProfile.class, () -> assumeSupported(sop::listProfiles) .subcommand("invalid")); } } diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java index b577017..501f53c 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java @@ -24,16 +24,16 @@ public class MergeCertsTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testMergeWithItself(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .key(key) .getBytes(); - byte[] merged = sop.mergeCerts() + byte[] merged = assumeSupported(sop::mergeCerts) .updates(cert) .baseCertificates(cert) .getBytes(); @@ -44,17 +44,17 @@ public class MergeCertsTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testMergeWithItselfArmored(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .noArmor() .userId("Alice ") .generate() .getBytes(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .key(key) .getBytes(); - byte[] merged = sop.mergeCerts() + byte[] merged = assumeSupported(sop::mergeCerts) .updates(cert) .baseCertificates(cert) .getBytes(); @@ -65,18 +65,18 @@ public class MergeCertsTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testMergeWithItselfViaBase(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .key(key) .getBytes(); byte[] certs = ArraysKt.plus(cert, cert); - byte[] merged = sop.mergeCerts() + byte[] merged = assumeSupported(sop::mergeCerts) .updates(cert) .baseCertificates(certs) .getBytes(); @@ -87,20 +87,20 @@ public class MergeCertsTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testApplyBaseToUpdate(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .key(key) .getBytes(); - byte[] update = sop.revokeKey() + byte[] update = assumeSupported(sop::revokeKey) .keys(key) .getBytes(); - byte[] merged = sop.mergeCerts() + byte[] merged = assumeSupported(sop::mergeCerts) .updates(cert) .baseCertificates(update) .getBytes(); @@ -111,20 +111,20 @@ public class MergeCertsTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testApplyUpdateToBase(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .key(key) .getBytes(); - byte[] update = sop.revokeKey() + byte[] update = assumeSupported(sop::revokeKey) .keys(key) .getBytes(); - byte[] merged = sop.mergeCerts() + byte[] merged = assumeSupported(sop::mergeCerts) .updates(update) .baseCertificates(cert) .getBytes(); @@ -135,25 +135,25 @@ public class MergeCertsTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testApplyUpdateToMissingBaseDoesNothing(SOP sop) throws IOException { - byte[] aliceKey = sop.generateKey() + byte[] aliceKey = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); - byte[] aliceCert = sop.extractCert() + byte[] aliceCert = assumeSupported(sop::extractCert) .key(aliceKey) .getBytes(); - byte[] bobKey = sop.generateKey() + byte[] bobKey = assumeSupported(sop::generateKey) .userId("Bob ") .generate() .getBytes(); - byte[] bobCert = sop.extractCert() + byte[] bobCert = assumeSupported(sop::extractCert) .key(bobKey) .getBytes(); - byte[] merged = sop.mergeCerts() + byte[] merged = assumeSupported(sop::mergeCerts) .updates(bobCert) .baseCertificates(aliceCert) .getBytes(); diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java index cb51332..1880d58 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java @@ -36,8 +36,8 @@ public class RevokeKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void revokeUnprotectedKey(SOP sop) throws IOException { - byte[] secretKey = sop.generateKey().userId("Alice ").generate().getBytes(); - byte[] revocation = sop.revokeKey().keys(secretKey).getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).userId("Alice ").generate().getBytes(); + byte[] revocation = assumeSupported(sop::revokeKey).keys(secretKey).getBytes(); assertTrue(JUtils.arrayStartsWith(revocation, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); assertFalse(Arrays.equals(secretKey, revocation)); @@ -46,8 +46,8 @@ public class RevokeKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void revokeUnprotectedKeyNoArmor(SOP sop) throws IOException { - byte[] secretKey = sop.generateKey().userId("Alice ").generate().getBytes(); - byte[] revocation = sop.revokeKey().noArmor().keys(secretKey).getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).userId("Alice ").generate().getBytes(); + byte[] revocation = assumeSupported(sop::revokeKey).noArmor().keys(secretKey).getBytes(); assertFalse(JUtils.arrayStartsWith(revocation, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); } @@ -55,8 +55,8 @@ public class RevokeKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void revokeUnprotectedKeyUnarmored(SOP sop) throws IOException { - byte[] secretKey = sop.generateKey().userId("Alice ").noArmor().generate().getBytes(); - byte[] revocation = sop.revokeKey().noArmor().keys(secretKey).getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).userId("Alice ").noArmor().generate().getBytes(); + byte[] revocation = assumeSupported(sop::revokeKey).noArmor().keys(secretKey).getBytes(); assertFalse(JUtils.arrayStartsWith(revocation, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); assertFalse(Arrays.equals(secretKey, revocation)); @@ -65,18 +65,18 @@ public class RevokeKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void revokeCertificateFails(SOP sop) throws IOException { - byte[] secretKey = sop.generateKey().generate().getBytes(); - byte[] certificate = sop.extractCert().key(secretKey).getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).generate().getBytes(); + byte[] certificate = assumeSupported(sop::extractCert).key(secretKey).getBytes(); - assertThrows(SOPGPException.BadData.class, () -> sop.revokeKey().keys(certificate).getBytes()); + assertThrows(SOPGPException.BadData.class, () -> assumeSupported(sop::revokeKey).keys(certificate).getBytes()); } @ParameterizedTest @MethodSource("provideInstances") public void revokeProtectedKey(SOP sop) throws IOException { byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); - byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice ").generate().getBytes(); - byte[] revocation = sop.revokeKey().withKeyPassword(password).keys(secretKey).getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice ").generate().getBytes(); + byte[] revocation = assumeSupported(sop::revokeKey).withKeyPassword(password).keys(secretKey).getBytes(); assertFalse(Arrays.equals(secretKey, revocation)); } @@ -86,8 +86,8 @@ public class RevokeKeyTest extends AbstractSOPTest { public void revokeProtectedKeyWithMultiplePasswordOptions(SOP sop) throws IOException { byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); byte[] wrongPassword = "0r4ng3".getBytes(UTF8Util.UTF8); - byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice ").generate().getBytes(); - byte[] revocation = sop.revokeKey().withKeyPassword(wrongPassword).withKeyPassword(password).keys(secretKey).getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice ").generate().getBytes(); + byte[] revocation = assumeSupported(sop::revokeKey).withKeyPassword(wrongPassword).withKeyPassword(password).keys(secretKey).getBytes(); assertFalse(Arrays.equals(secretKey, revocation)); } @@ -96,9 +96,9 @@ public class RevokeKeyTest extends AbstractSOPTest { @MethodSource("provideInstances") public void revokeProtectedKeyWithMissingPassphraseFails(SOP sop) throws IOException { byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); - byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice ").generate().getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice ").generate().getBytes(); - assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.revokeKey().keys(secretKey).getBytes()); + assertThrows(SOPGPException.KeyIsProtected.class, () -> assumeSupported(sop::revokeKey).keys(secretKey).getBytes()); } @ParameterizedTest @@ -106,27 +106,27 @@ public class RevokeKeyTest extends AbstractSOPTest { public void revokeProtectedKeyWithWrongPassphraseFails(SOP sop) throws IOException { byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); String wrongPassword = "or4ng3"; - byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice ").generate().getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice ").generate().getBytes(); - assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.revokeKey().withKeyPassword(wrongPassword).keys(secretKey).getBytes()); + assertThrows(SOPGPException.KeyIsProtected.class, () -> assumeSupported(sop::revokeKey).withKeyPassword(wrongPassword).keys(secretKey).getBytes()); } @ParameterizedTest @MethodSource("provideInstances") public void revokeKeyIsNowHardRevoked(SOP sop) throws IOException { - byte[] key = sop.generateKey().generate().getBytes(); - byte[] cert = sop.extractCert().key(key).getBytes(); + byte[] key = assumeSupported(sop::generateKey).generate().getBytes(); + byte[] cert = assumeSupported(sop::extractCert).key(key).getBytes(); // Sign a message with the key byte[] msg = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signedMsg = sop.inlineSign().key(key).data(msg).getBytes(); + byte[] signedMsg = assumeSupported(sop::inlineSign).key(key).data(msg).getBytes(); // Verifying the message with the valid cert works - List result = sop.inlineVerify().cert(cert).data(signedMsg).toByteArrayAndResult().getResult(); + List result = assumeSupported(sop::inlineVerify).cert(cert).data(signedMsg).toByteArrayAndResult().getResult(); VerificationListAssert.assertThatVerificationList(result).hasSingleItem(); // Now hard revoke the key and re-check signature, expecting no valid certification - byte[] revokedCert = sop.revokeKey().keys(key).getBytes(); - assertThrows(SOPGPException.NoSignature.class, () -> sop.inlineVerify().cert(revokedCert).data(signedMsg).toByteArrayAndResult()); + byte[] revokedCert = assumeSupported(sop::revokeKey).keys(key).getBytes(); + assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::inlineVerify).cert(revokedCert).data(signedMsg).toByteArrayAndResult()); } } diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java index 47644bf..71f7efd 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java @@ -28,7 +28,7 @@ public class VersionTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void versionNameTest(SOP sop) { - String name = sop.version().getName(); + String name = assumeSupported(sop::version).getName(); assertNotNull(name); assertFalse(name.isEmpty()); } @@ -36,21 +36,21 @@ public class VersionTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void versionVersionTest(SOP sop) { - String version = sop.version().getVersion(); + String version = assumeSupported(sop::version).getVersion(); assertFalse(version.isEmpty()); } @ParameterizedTest @MethodSource("provideInstances") public void backendVersionTest(SOP sop) { - String backend = sop.version().getBackendVersion(); + String backend = assumeSupported(sop::version).getBackendVersion(); assertFalse(backend.isEmpty()); } @ParameterizedTest @MethodSource("provideInstances") public void extendedVersionTest(SOP sop) { - String extended = sop.version().getExtendedVersion(); + String extended = assumeSupported(sop::version).getExtendedVersion(); assertFalse(extended.isEmpty()); } @@ -58,27 +58,27 @@ public class VersionTest extends AbstractSOPTest { @MethodSource("provideInstances") public void sopSpecVersionTest(SOP sop) { try { - sop.version().getSopSpecVersion(); + assumeSupported(sop::version).getSopSpecVersion(); } catch (RuntimeException e) { throw new TestAbortedException("SOP backend does not support 'version --sop-spec' yet."); } - String sopSpec = sop.version().getSopSpecVersion(); - if (sop.version().isSopSpecImplementationIncomplete()) { + String sopSpec = assumeSupported(sop::version).getSopSpecVersion(); + if (assumeSupported(sop::version).isSopSpecImplementationIncomplete()) { assertTrue(sopSpec.startsWith("~draft-dkg-openpgp-stateless-cli-")); } else { assertTrue(sopSpec.startsWith("draft-dkg-openpgp-stateless-cli-")); } - int sopRevision = sop.version().getSopSpecRevisionNumber(); - assertTrue(sop.version().getSopSpecRevisionName().endsWith("" + sopRevision)); + int sopRevision = assumeSupported(sop::version).getSopSpecRevisionNumber(); + assertTrue(assumeSupported(sop::version).getSopSpecRevisionName().endsWith("" + sopRevision)); } @ParameterizedTest @MethodSource("provideInstances") public void sopVVersionTest(SOP sop) { try { - sop.version().getSopVVersion(); + assumeSupported(sop::version).getSopVVersion(); } catch (SOPGPException.UnsupportedOption e) { throw new TestAbortedException( "Implementation does (gracefully) not provide coverage for any sopv interface version."); @@ -90,6 +90,6 @@ public class VersionTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void sopJavaVersionTest(SOP sop) { - assertNotNull(sop.version().getSopJavaVersion()); + assertNotNull(assumeSupported(sop::version).getSopJavaVersion()); } } diff --git a/sop-java/src/test/java/sop/VerificationJSONTest.java b/sop-java/src/test/java/sop/VerificationJSONTest.java index a80e6fc..a6e5d96 100644 --- a/sop-java/src/test/java/sop/VerificationJSONTest.java +++ b/sop-java/src/test/java/sop/VerificationJSONTest.java @@ -7,6 +7,7 @@ package sop; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import sop.enums.SignatureMode; +import sop.testsuite.assertions.VerificationAssert; import java.text.ParseException; import java.util.ArrayList; @@ -158,6 +159,8 @@ public class VerificationJSONTest { assertNull(json.getExt()); verification = new Verification(verification.getCreationTime(), verification.getSigningKeyFingerprint(), verification.getSigningCertFingerprint(), verification.getSignatureMode().get(), json, dummySerializer); + VerificationAssert.assertThatVerification(verification) + .hasJSON(dummyParser, j -> j.getSigners().contains("alice.pgp")); assertEquals(string, verification.toString()); } } From d32d9b54d75586c834d73b08a6b2c48d7b9d3ddc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 1 Jul 2025 15:06:50 +0200 Subject: [PATCH 294/298] Depend on junit-platform-suite to avoid needing to inherit test suite for external-sop tests --- external-sop/build.gradle | 4 +++- .../sop/testsuite/external/ExternalTestSuite.java | 14 ++++++++++++++ .../operation/ExternalArmorDearmorTest.java | 13 ------------- .../ExternalCertifyValidateUserIdTest.java | 13 ------------- .../operation/ExternalChangeKeyPasswordTest.java | 13 ------------- .../ExternalDecryptWithSessionKeyTest.java | 13 ------------- .../ExternalDetachedSignDetachedVerifyTest.java | 12 ------------ .../operation/ExternalEncryptDecryptTest.java | 13 ------------- .../operation/ExternalExtractCertTest.java | 13 ------------- .../operation/ExternalGenerateKeyTest.java | 13 ------------- ...alInlineSignInlineDetachDetachedVerifyTest.java | 14 -------------- .../ExternalInlineSignInlineVerifyTest.java | 13 ------------- .../operation/ExternalListProfilesTest.java | 13 ------------- .../external/operation/ExternalMergeCertsTest.java | 13 ------------- .../external/operation/ExternalRevokeKeyTest.java | 13 ------------- .../external/operation/ExternalVersionTest.java | 13 ------------- 16 files changed, 17 insertions(+), 183 deletions(-) create mode 100644 external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalArmorDearmorTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalDecryptWithSessionKeyTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalDetachedSignDetachedVerifyTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalEncryptDecryptTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalExtractCertTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalGenerateKeyTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineDetachDetachedVerifyTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineVerifyTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalListProfilesTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalVersionTest.java diff --git a/external-sop/build.gradle b/external-sop/build.gradle index d1a7ffb..2dfbf7e 100644 --- a/external-sop/build.gradle +++ b/external-sop/build.gradle @@ -15,7 +15,9 @@ repositories { 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" + + testImplementation "org.junit.platform:junit-platform-suite-api:1.13.2" + testRuntimeOnly 'org.junit.platform:junit-platform-suite:1.13.2' api project(":sop-java") api "org.slf4j:slf4j-api:$slf4jVersion" diff --git a/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java b/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java new file mode 100644 index 0000000..6e991cf --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java @@ -0,0 +1,14 @@ +package sop.testsuite.external; + +import org.junit.platform.suite.api.IncludeClassNamePatterns; +import org.junit.platform.suite.api.SelectPackages; +import org.junit.platform.suite.api.Suite; +import org.junit.platform.suite.api.SuiteDisplayName; + +@Suite +@SuiteDisplayName("External SOP Tests") +@SelectPackages("sop.testsuite.operation") +@IncludeClassNamePatterns(".*Test") +public class ExternalTestSuite { + +} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalArmorDearmorTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalArmorDearmorTest.java deleted file mode 100644 index 1d8ff2b..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalArmorDearmorTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.ArmorDearmorTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalArmorDearmorTest extends ArmorDearmorTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java deleted file mode 100644 index bb319ca..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.CertifyValidateUserIdTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalCertifyValidateUserIdTest extends CertifyValidateUserIdTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java deleted file mode 100644 index 42a9693..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.ChangeKeyPasswordTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalChangeKeyPasswordTest extends ChangeKeyPasswordTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalDecryptWithSessionKeyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalDecryptWithSessionKeyTest.java deleted file mode 100644 index 0ac03a4..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalDecryptWithSessionKeyTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.DecryptWithSessionKeyTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalDecryptWithSessionKeyTest extends DecryptWithSessionKeyTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalDetachedSignDetachedVerifyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalDetachedSignDetachedVerifyTest.java deleted file mode 100644 index 13959df..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalDetachedSignDetachedVerifyTest.java +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.DetachedSignDetachedVerifyTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalDetachedSignDetachedVerifyTest extends DetachedSignDetachedVerifyTest { -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalEncryptDecryptTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalEncryptDecryptTest.java deleted file mode 100644 index b83ca46..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalEncryptDecryptTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.EncryptDecryptTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalEncryptDecryptTest extends EncryptDecryptTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalExtractCertTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalExtractCertTest.java deleted file mode 100644 index f47656c..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalExtractCertTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.ExtractCertTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalExtractCertTest extends ExtractCertTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalGenerateKeyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalGenerateKeyTest.java deleted file mode 100644 index 7ac971b..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalGenerateKeyTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.GenerateKeyTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalGenerateKeyTest extends GenerateKeyTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineDetachDetachedVerifyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineDetachDetachedVerifyTest.java deleted file mode 100644 index 2dd3396..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineDetachDetachedVerifyTest.java +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.InlineSignInlineDetachDetachedVerifyTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalInlineSignInlineDetachDetachedVerifyTest - extends InlineSignInlineDetachDetachedVerifyTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineVerifyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineVerifyTest.java deleted file mode 100644 index 24e30aa..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineVerifyTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.InlineSignInlineVerifyTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalInlineSignInlineVerifyTest extends InlineSignInlineVerifyTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalListProfilesTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalListProfilesTest.java deleted file mode 100644 index 18da883..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalListProfilesTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.ListProfilesTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalListProfilesTest extends ListProfilesTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java deleted file mode 100644 index 8b22b37..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.MergeCertsTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalMergeCertsTest extends MergeCertsTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java deleted file mode 100644 index e2efe03..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.RevokeKeyTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalRevokeKeyTest extends RevokeKeyTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalVersionTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalVersionTest.java deleted file mode 100644 index ee63f09..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalVersionTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.VersionTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalVersionTest extends VersionTest { - -} From d4e8c14b08ccb5d36c1ebc98bfa06d7f80061dd0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Jul 2025 12:53:27 +0200 Subject: [PATCH 295/298] Update documentation, add withKeyPassphrase(CharArray) methods --- .../main/kotlin/sop/operation/AbstractSign.kt | 13 ++- .../src/main/kotlin/sop/operation/Armor.kt | 1 + .../kotlin/sop/operation/CertifyUserId.kt | 102 +++++++++++++++--- .../kotlin/sop/operation/ChangeKeyPassword.kt | 51 ++++++--- .../src/main/kotlin/sop/operation/Dearmor.kt | 1 + .../src/main/kotlin/sop/operation/Decrypt.kt | 1 + .../main/kotlin/sop/operation/DetachedSign.kt | 1 + .../kotlin/sop/operation/DetachedVerify.kt | 1 + .../src/main/kotlin/sop/operation/Encrypt.kt | 1 + .../main/kotlin/sop/operation/ExtractCert.kt | 1 + .../main/kotlin/sop/operation/GenerateKey.kt | 1 + .../main/kotlin/sop/operation/InlineDetach.kt | 1 + .../main/kotlin/sop/operation/InlineSign.kt | 1 + .../main/kotlin/sop/operation/InlineVerify.kt | 2 +- .../main/kotlin/sop/operation/ListProfiles.kt | 2 +- .../main/kotlin/sop/operation/MergeCerts.kt | 53 +++++++-- .../main/kotlin/sop/operation/RevokeKey.kt | 38 ++++++- .../main/kotlin/sop/operation/UpdateKey.kt | 66 ++++++++---- .../kotlin/sop/operation/ValidateUserId.kt | 51 +++++---- .../kotlin/sop/operation/VerifySignatures.kt | 1 + .../src/main/kotlin/sop/operation/Version.kt | 1 + 21 files changed, 308 insertions(+), 82 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt b/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt index 0258432..72b8f72 100644 --- a/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt +++ b/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt @@ -61,9 +61,18 @@ interface AbstractSign { * @param password password * @return builder instance * @throws UnsupportedOption if key passwords are not supported - * @throws PasswordNotHumanReadable if the provided passphrase is not human-readable */ - @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + @Throws(UnsupportedOption::class) + fun withKeyPassword(password: CharArray): T = withKeyPassword(password.concatToString()) + + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if key passwords are not supported + */ + @Throws(UnsupportedOption::class) fun withKeyPassword(password: String): T = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) /** diff --git a/sop-java/src/main/kotlin/sop/operation/Armor.kt b/sop-java/src/main/kotlin/sop/operation/Armor.kt index be7f1a3..b54aed7 100644 --- a/sop-java/src/main/kotlin/sop/operation/Armor.kt +++ b/sop-java/src/main/kotlin/sop/operation/Armor.kt @@ -9,6 +9,7 @@ import java.io.InputStream import sop.Ready import sop.exception.SOPGPException.BadData +/** Interface for armoring binary OpenPGP data. */ interface Armor { /** diff --git a/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt index 642966b..d59f9f0 100644 --- a/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt +++ b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt @@ -7,35 +7,111 @@ package sop.operation import java.io.IOException import java.io.InputStream import sop.Ready -import sop.exception.SOPGPException +import sop.exception.SOPGPException.* import sop.util.UTF8Util +/** Interface for issuing certifications over UserIDs on certificates. */ interface CertifyUserId { - @Throws(SOPGPException.UnsupportedOption::class) fun noArmor(): CertifyUserId + /** Disable ASCII armor for the output. */ + @Throws(UnsupportedOption::class) fun noArmor(): CertifyUserId - @Throws(SOPGPException.UnsupportedOption::class) fun userId(userId: String): CertifyUserId + /** + * Add a user-id that shall be certified on the certificates. + * + * @param userId user-id + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun userId(userId: String): CertifyUserId - @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if key passwords are not supported + */ + @Throws(UnsupportedOption::class) + fun withKeyPassword(password: CharArray): CertifyUserId = + withKeyPassword(password.concatToString()) + + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if key passwords are not supported + */ + @Throws(UnsupportedOption::class) fun withKeyPassword(password: String): CertifyUserId = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) - @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if key passwords are not supported + * @throws PasswordNotHumanReadable if the provided password is not human-readable + */ + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) fun withKeyPassword(password: ByteArray): CertifyUserId - @Throws(SOPGPException.UnsupportedOption::class) fun noRequireSelfSig(): CertifyUserId + /** + * If this option is provided, it is possible to certify user-ids on certificates, which do not + * have a self-certification for the user-id. You can use this option to add pet-name + * certifications to certificates, e.g. "Mom". + * + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun noRequireSelfSig(): CertifyUserId - @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class) - fun keys(keys: InputStream): CertifyUserId + /** + * Provide signing keys for issuing the certifications. + * + * @param keys input stream containing one or more signing key + * @return builder instance + * @throws BadData if the keys cannot be read + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, IOException::class) fun keys(keys: InputStream): CertifyUserId - @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class) + /** + * Provide signing keys for issuing the certifications. + * + * @param keys byte array containing one or more signing key + * @return builder instance + * @throws BadData if the keys cannot be read + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, IOException::class) fun keys(keys: ByteArray): CertifyUserId = keys(keys.inputStream()) - @Throws( - SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + /** + * Provide the certificates that you want to create certifications for. + * + * @param certs input stream containing the certificates + * @return object to require the certified certificates from + * @throws BadData if the certificates cannot be read + * @throws IOException if an IO error occurs + * @throws KeyIsProtected if one or more signing keys are passphrase protected and cannot be + * unlocked + */ + @Throws(BadData::class, IOException::class, CertUserIdNoMatch::class, KeyIsProtected::class) fun certs(certs: InputStream): Ready - @Throws( - SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + /** + * Provide the certificates that you want to create certifications for. + * + * @param certs byte array containing the certificates + * @return object to require the certified certificates from + * @throws BadData if the certificates cannot be read + * @throws IOException if an IO error occurs + * @throws KeyIsProtected if one or more signing keys are passphrase protected and cannot be + * unlocked + */ + @Throws(BadData::class, IOException::class, CertUserIdNoMatch::class, KeyIsProtected::class) fun certs(certs: ByteArray): Ready = certs(certs.inputStream()) } diff --git a/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt b/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt index 224e0f4..fe9b8c9 100644 --- a/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt +++ b/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt @@ -11,6 +11,7 @@ import sop.exception.SOPGPException.KeyIsProtected import sop.exception.SOPGPException.PasswordNotHumanReadable import sop.util.UTF8Util +/** Interface for changing key passwords. */ interface ChangeKeyPassword { /** @@ -28,13 +29,8 @@ interface ChangeKeyPassword { * @param oldPassphrase old passphrase * @return builder instance */ - @Throws(PasswordNotHumanReadable::class) - fun oldKeyPassphrase(oldPassphrase: ByteArray): ChangeKeyPassword = - try { - oldKeyPassphrase(UTF8Util.decodeUTF8(oldPassphrase)) - } catch (e: CharacterCodingException) { - throw PasswordNotHumanReadable("Password MUST be a valid UTF8 string.") - } + fun oldKeyPassphrase(oldPassphrase: CharArray): ChangeKeyPassword = + oldKeyPassphrase(oldPassphrase.concatToString()) /** * Provide a passphrase to unlock the secret key. This method can be provided multiple times to @@ -47,21 +43,33 @@ interface ChangeKeyPassword { fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword /** - * Provide a passphrase to re-lock the secret key with. This method can only be used once, and - * all key material encountered will be encrypted with the given passphrase. If this method is - * not called, the key material will not be protected. + * Provide a passphrase to unlock the secret key. This method can be provided multiple times to + * provide separate passphrases that are tried as a means to unlock any secret key material + * encountered. * - * @param newPassphrase new passphrase + * @param oldPassphrase old passphrase * @return builder instance + * @throws PasswordNotHumanReadable if the old key passphrase is not human-readable */ @Throws(PasswordNotHumanReadable::class) - fun newKeyPassphrase(newPassphrase: ByteArray): ChangeKeyPassword = + fun oldKeyPassphrase(oldPassphrase: ByteArray): ChangeKeyPassword = try { - newKeyPassphrase(UTF8Util.decodeUTF8(newPassphrase)) + oldKeyPassphrase(UTF8Util.decodeUTF8(oldPassphrase)) } catch (e: CharacterCodingException) { throw PasswordNotHumanReadable("Password MUST be a valid UTF8 string.") } + /** + * Provide a passphrase to re-lock the secret key with. This method can only be used once, and + * all key material encountered will be encrypted with the given passphrase. If this method is + * not called, the key material will not be protected. + * + * @param newPassphrase new passphrase + * @return builder instance + */ + fun newKeyPassphrase(newPassphrase: CharArray): ChangeKeyPassword = + newKeyPassphrase(newPassphrase.concatToString()) + /** * Provide a passphrase to re-lock the secret key with. This method can only be used once, and * all key material encountered will be encrypted with the given passphrase. If this method is @@ -72,6 +80,23 @@ interface ChangeKeyPassword { */ fun newKeyPassphrase(newPassphrase: String): ChangeKeyPassword + /** + * Provide a passphrase to re-lock the secret key with. This method can only be used once, and + * all key material encountered will be encrypted with the given passphrase. If this method is + * not called, the key material will not be protected. + * + * @param newPassphrase new passphrase + * @return builder instance + * @throws PasswordNotHumanReadable if the passphrase is not human-readable + */ + @Throws(PasswordNotHumanReadable::class) + fun newKeyPassphrase(newPassphrase: ByteArray): ChangeKeyPassword = + try { + newKeyPassphrase(UTF8Util.decodeUTF8(newPassphrase)) + } catch (e: CharacterCodingException) { + throw PasswordNotHumanReadable("Password MUST be a valid UTF8 string.") + } + /** * Provide the key material. * diff --git a/sop-java/src/main/kotlin/sop/operation/Dearmor.kt b/sop-java/src/main/kotlin/sop/operation/Dearmor.kt index cc5e98d..2984f27 100644 --- a/sop-java/src/main/kotlin/sop/operation/Dearmor.kt +++ b/sop-java/src/main/kotlin/sop/operation/Dearmor.kt @@ -10,6 +10,7 @@ import sop.Ready import sop.exception.SOPGPException.BadData import sop.util.UTF8Util +/** Interface for removing ASCII armor from OpenPGP data. */ interface Dearmor { /** diff --git a/sop-java/src/main/kotlin/sop/operation/Decrypt.kt b/sop-java/src/main/kotlin/sop/operation/Decrypt.kt index ae228e9..4d009f9 100644 --- a/sop-java/src/main/kotlin/sop/operation/Decrypt.kt +++ b/sop-java/src/main/kotlin/sop/operation/Decrypt.kt @@ -13,6 +13,7 @@ import sop.SessionKey import sop.exception.SOPGPException.* import sop.util.UTF8Util +/** Interface for decrypting encrypted OpenPGP messages. */ interface Decrypt { /** diff --git a/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt b/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt index c0e62dd..4aaadc1 100644 --- a/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt +++ b/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt @@ -11,6 +11,7 @@ import sop.SigningResult import sop.enums.SignAs import sop.exception.SOPGPException.* +/** Interface for creating detached signatures over plaintext messages. */ interface DetachedSign : AbstractSign { /** diff --git a/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt b/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt index d899b54..319658d 100644 --- a/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt +++ b/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt @@ -8,6 +8,7 @@ import java.io.IOException import java.io.InputStream import sop.exception.SOPGPException.BadData +/** Interface for verifying detached OpenPGP signatures over plaintext messages. */ interface DetachedVerify : AbstractVerify, VerifySignatures { /** diff --git a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt index 71c04cb..02c7f97 100644 --- a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt +++ b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt @@ -13,6 +13,7 @@ import sop.enums.EncryptAs import sop.exception.SOPGPException.* import sop.util.UTF8Util +/** Interface for creating encrypted OpenPGP messages. */ interface Encrypt { /** diff --git a/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt b/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt index e2ce1cc..6485bc2 100644 --- a/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt +++ b/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt @@ -9,6 +9,7 @@ import java.io.InputStream import sop.Ready import sop.exception.SOPGPException.BadData +/** Interface for extracting certificates from OpenPGP keys. */ interface ExtractCert { /** diff --git a/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt b/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt index 13de39a..bccd372 100644 --- a/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt @@ -10,6 +10,7 @@ import sop.Ready import sop.exception.SOPGPException.* import sop.util.UTF8Util +/** Interface for generating OpenPGP keys. */ interface GenerateKey { /** diff --git a/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt b/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt index 941a9bf..1cc64ce 100644 --- a/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt +++ b/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt @@ -10,6 +10,7 @@ import sop.ReadyWithResult import sop.Signatures import sop.exception.SOPGPException.BadData +/** Interface for detaching inline signatures from OpenPGP messages. */ interface InlineDetach { /** diff --git a/sop-java/src/main/kotlin/sop/operation/InlineSign.kt b/sop-java/src/main/kotlin/sop/operation/InlineSign.kt index 11b5668..6855a61 100644 --- a/sop-java/src/main/kotlin/sop/operation/InlineSign.kt +++ b/sop-java/src/main/kotlin/sop/operation/InlineSign.kt @@ -10,6 +10,7 @@ import sop.Ready import sop.enums.InlineSignAs import sop.exception.SOPGPException.* +/** Interface for creating inline-signed OpenPGP messages. */ interface InlineSign : AbstractSign { /** diff --git a/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt b/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt index c16b269..a944957 100644 --- a/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt +++ b/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt @@ -11,7 +11,7 @@ import sop.Verification import sop.exception.SOPGPException.BadData import sop.exception.SOPGPException.NoSignature -/** API for verification of inline-signed messages. */ +/** Interface for verification of inline-signed messages. */ interface InlineVerify : AbstractVerify { /** diff --git a/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt b/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt index 315faf2..0bed1f8 100644 --- a/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt +++ b/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt @@ -6,7 +6,7 @@ package sop.operation import sop.Profile -/** Subcommand to list supported profiles of other subcommands. */ +/** Interface to list supported profiles of other subcommands. */ interface ListProfiles { /** diff --git a/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt index f922490..20469cb 100644 --- a/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt +++ b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt @@ -7,21 +7,58 @@ package sop.operation import java.io.IOException import java.io.InputStream import sop.Ready -import sop.exception.SOPGPException +import sop.exception.SOPGPException.* +/** Interface for merging multiple copies of the same certificate into one. */ interface MergeCerts { - @Throws(SOPGPException.UnsupportedOption::class) fun noArmor(): MergeCerts + /** + * Disable ASCII armor for the output certificate. + * + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun noArmor(): MergeCerts - @Throws(SOPGPException.BadData::class, IOException::class) - fun updates(updateCerts: InputStream): MergeCerts + /** + * Provide updated copies of the base certificate. + * + * @param updateCerts input stream containing an updated copy of the base cert + * @return builder instance + * @throws BadData if the update cannot be read + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, IOException::class) fun updates(updateCerts: InputStream): MergeCerts - @Throws(SOPGPException.BadData::class, IOException::class) + /** + * Provide updated copies of the base certificate. + * + * @param updateCerts byte array containing an updated copy of the base cert + * @return builder instance + * @throws BadData if the update cannot be read + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, IOException::class) fun updates(updateCerts: ByteArray): MergeCerts = updates(updateCerts.inputStream()) - @Throws(SOPGPException.BadData::class, IOException::class) - fun baseCertificates(certs: InputStream): Ready + /** + * Provide the base certificate into which updates shall be merged. + * + * @param certs input stream containing the base OpenPGP certificate + * @return object to require the merged certificate from + * @throws BadData if the base certificate cannot be read + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, IOException::class) fun baseCertificates(certs: InputStream): Ready - @Throws(SOPGPException.BadData::class, IOException::class) + /** + * Provide the base certificate into which updates shall be merged. + * + * @param certs byte array containing the base OpenPGP certificate + * @return object to require the merged certificate from + * @throws BadData if the base certificate cannot be read + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, IOException::class) fun baseCertificates(certs: ByteArray): Ready = baseCertificates(certs.inputStream()) } diff --git a/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt b/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt index f3cbe5c..13c6712 100644 --- a/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt @@ -4,12 +4,13 @@ package sop.operation +import java.io.IOException import java.io.InputStream import sop.Ready -import sop.exception.SOPGPException.PasswordNotHumanReadable -import sop.exception.SOPGPException.UnsupportedOption +import sop.exception.SOPGPException.* import sop.util.UTF8Util +/** Interface for creating certificate revocations. */ interface RevokeKey { /** @@ -25,9 +26,18 @@ interface RevokeKey { * @param password password * @return builder instance * @throws UnsupportedOption if the implementation does not support key passwords - * @throws PasswordNotHumanReadable if the password is not human-readable */ - @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + @Throws(UnsupportedOption::class) + fun withKeyPassword(password: CharArray): RevokeKey = withKeyPassword(password.concatToString()) + + /** + * Provide the decryption password for the secret key. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if the implementation does not support key passwords + */ + @Throws(UnsupportedOption::class) fun withKeyPassword(password: String): RevokeKey = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) @@ -42,7 +52,27 @@ interface RevokeKey { @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) fun withKeyPassword(password: ByteArray): RevokeKey + /** + * Provide the key that you want to revoke. + * + * @param bytes byte array containing the OpenPGP key + * @return object to require the revocation certificate from + * @throws BadData if the key cannot be read + * @throws KeyIsProtected if the key is protected and cannot be unlocked + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, KeyIsProtected::class, IOException::class) fun keys(bytes: ByteArray): Ready = keys(bytes.inputStream()) + /** + * Provide the key that you want to revoke. + * + * @param keys input stream containing the OpenPGP key + * @return object to require the revocation certificate from + * @throws BadData if the key cannot be read + * @throws KeyIsProtected if the key is protected and cannot be unlocked + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, KeyIsProtected::class, IOException::class) fun keys(keys: InputStream): Ready } diff --git a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt index 1226ed5..13a4bdc 100644 --- a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt @@ -7,9 +7,10 @@ package sop.operation import java.io.IOException import java.io.InputStream import sop.Ready -import sop.exception.SOPGPException +import sop.exception.SOPGPException.* import sop.util.UTF8Util +/** Interface for bringing an OpenPGP key up to date. */ interface UpdateKey { /** @@ -22,21 +23,39 @@ interface UpdateKey { /** * Allow key to be used for signing only. If this option is not present, the operation may add a * new, encryption-capable component key. + * + * @return builder instance + * @throws UnsupportedOption if this option is not supported */ - @Throws(SOPGPException.UnsupportedOption::class) fun signingOnly(): UpdateKey + @Throws(UnsupportedOption::class) fun signingOnly(): UpdateKey /** * Do not allow adding new capabilities to the key. If this option is not present, the operation * may add support for new capabilities to the key. + * + * @return builder instance + * @throws UnsupportedOption if this option is not supported */ - @Throws(SOPGPException.UnsupportedOption::class) fun noAddedCapabilities(): UpdateKey + @Throws(UnsupportedOption::class) fun noAddedCapabilities(): UpdateKey /** * Provide a passphrase for unlocking the secret key. * * @param password password + * @return builder instance + * @throws UnsupportedOption if this option is not supported */ - @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + @Throws(UnsupportedOption::class) + fun withKeyPassword(password: CharArray): UpdateKey = withKeyPassword(password.concatToString()) + + /** + * Provide a passphrase for unlocking the secret key. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun withKeyPassword(password: String): UpdateKey = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) @@ -44,8 +63,11 @@ interface UpdateKey { * Provide a passphrase for unlocking the secret key. * * @param password password + * @return builder instance + * @throws PasswordNotHumanReadable if the password is not human-readable + * @throws UnsupportedOption if this option is not supported */ - @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) fun withKeyPassword(password: ByteArray): UpdateKey /** @@ -53,9 +75,12 @@ interface UpdateKey { * These certificates will be merged into the key. * * @param certs input stream of certificates + * @return builder instance + * @throws UnsupportedOption if this option is not supported + * @throws BadData if the certificate cannot be read + * @throws IOException if an IO error occurs */ - @Throws( - SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) + @Throws(UnsupportedOption::class, BadData::class, IOException::class) fun mergeCerts(certs: InputStream): UpdateKey /** @@ -63,9 +88,12 @@ interface UpdateKey { * These certificates will be merged into the key. * * @param certs binary certificates + * @return builder instance + * @throws UnsupportedOption if this option is not supported + * @throws BadData if the certificate cannot be read + * @throws IOException if an IO error occurs */ - @Throws( - SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) + @Throws(UnsupportedOption::class, BadData::class, IOException::class) fun mergeCerts(certs: ByteArray): UpdateKey = mergeCerts(certs.inputStream()) /** @@ -73,12 +101,12 @@ interface UpdateKey { * * @param key input stream containing the key * @return handle to acquire the updated OpenPGP key from + * @throws BadData if the key cannot be read + * @throws IOException if an IO error occurs + * @throws KeyIsProtected if the key is passphrase protected and cannot be unlocked + * @throws PrimaryKeyBad if the primary key is bad (e.g. expired, too weak) */ - @Throws( - SOPGPException.BadData::class, - IOException::class, - SOPGPException.KeyIsProtected::class, - SOPGPException.PrimaryKeyBad::class) + @Throws(BadData::class, IOException::class, KeyIsProtected::class, PrimaryKeyBad::class) fun key(key: InputStream): Ready /** @@ -86,11 +114,11 @@ interface UpdateKey { * * @param key binary OpenPGP key * @return handle to acquire the updated OpenPGP key from + * @throws BadData if the key cannot be read + * @throws IOException if an IO error occurs + * @throws KeyIsProtected if the key is passphrase protected and cannot be unlocked + * @throws PrimaryKeyBad if the primary key is bad (e.g. expired, too weak) */ - @Throws( - SOPGPException.BadData::class, - IOException::class, - SOPGPException.KeyIsProtected::class, - SOPGPException.PrimaryKeyBad::class) + @Throws(BadData::class, IOException::class, KeyIsProtected::class, PrimaryKeyBad::class) fun key(key: ByteArray): Ready = key(key.inputStream()) } diff --git a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt index fe20fd4..971de25 100644 --- a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt +++ b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt @@ -7,9 +7,9 @@ package sop.operation import java.io.IOException import java.io.InputStream import java.util.* -import sop.exception.SOPGPException +import sop.exception.SOPGPException.* -/** Subcommand to validate UserIDs on certificates. */ +/** Interface to validate UserIDs on certificates. */ interface ValidateUserId { /** @@ -17,15 +17,16 @@ interface ValidateUserId { * e-mail address part of each correctly bound User ID. The rest of each correctly bound User ID * is ignored. * - * @return this + * @return builder instance + * @throws UnsupportedOption if this option is not supported */ - @Throws(SOPGPException.UnsupportedOption::class) fun addrSpecOnly(): ValidateUserId + @Throws(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 + * @return builder instance */ fun userId(userId: String): ValidateUserId @@ -34,19 +35,22 @@ interface ValidateUserId { * if it was bound by an authoritative certificate. * * @param certs authoritative certificates - * @return this + * @return builder instance + * @throws BadData if the authority certificates cannot be read + * @throws IOException if an IO error occurs */ - @Throws(SOPGPException.BadData::class, IOException::class) - fun authorities(certs: InputStream): ValidateUserId + @Throws(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 + * @return builder instance + * @throws BadData if the authority certificates cannot be read + * @throws IOException if an IO error occurs */ - @Throws(SOPGPException.BadData::class, IOException::class) + @Throws(BadData::class, IOException::class) fun authorities(certs: ByteArray): ValidateUserId = authorities(certs.inputStream()) /** @@ -54,13 +58,12 @@ interface ValidateUserId { * * @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 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 CertUserIdNoMatch if any subject certificate does not have a correctly bound UserID + * that matches [userId]. */ - @Throws( - SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + @Throws(BadData::class, IOException::class, CertUserIdNoMatch::class) fun subjects(certs: InputStream): Boolean /** @@ -68,14 +71,20 @@ interface ValidateUserId { * * @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 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 CertUserIdNoMatch if any subject certificate does not have a correctly bound UserID + * that matches [userId]. */ - @Throws( - SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + @Throws(BadData::class, IOException::class, CertUserIdNoMatch::class) fun subjects(certs: ByteArray): Boolean = subjects(certs.inputStream()) - fun validateAt(date: Date): ValidateUserId + /** + * Provide a reference time for user-id validation. + * + * @param date reference time + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun validateAt(date: Date): ValidateUserId } diff --git a/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt b/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt index b75e4a5..00a64aa 100644 --- a/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt +++ b/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt @@ -10,6 +10,7 @@ import sop.Verification import sop.exception.SOPGPException.BadData import sop.exception.SOPGPException.NoSignature +/** API handle for verifying signatures. */ interface VerifySignatures { /** diff --git a/sop-java/src/main/kotlin/sop/operation/Version.kt b/sop-java/src/main/kotlin/sop/operation/Version.kt index 6c8aa95..8a4c808 100644 --- a/sop-java/src/main/kotlin/sop/operation/Version.kt +++ b/sop-java/src/main/kotlin/sop/operation/Version.kt @@ -10,6 +10,7 @@ import java.util.* import kotlin.jvm.Throws import sop.exception.SOPGPException +/** Interface for acquiring version information about the SOP implementation. */ interface Version { /** From e680f3450a392aab2569fd97e6e993ba65fd63af Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Jul 2025 12:21:06 +0200 Subject: [PATCH 296/298] SOPV: Document since when operations are available --- sop-java/src/main/kotlin/sop/SOPV.kt | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/SOPV.kt b/sop-java/src/main/kotlin/sop/SOPV.kt index 27eb6e3..d483194 100644 --- a/sop-java/src/main/kotlin/sop/SOPV.kt +++ b/sop-java/src/main/kotlin/sop/SOPV.kt @@ -12,27 +12,41 @@ import sop.operation.Version /** Subset of [SOP] implementing only OpenPGP signature verification. */ interface SOPV { - /** Get information about the implementations name and version. */ + /** + * Get information about the implementations name and version. + * + * @since sopv 1.0 + */ fun version(): Version? /** * Verify detached signatures. If you need to verify an inline-signed message, use * [inlineVerify] instead. + * + * @since sopv 1.0 */ fun verify(): DetachedVerify? = detachedVerify() /** * Verify detached signatures. If you need to verify an inline-signed message, use * [inlineVerify] instead. + * + * @since sopv 1.0 */ fun detachedVerify(): DetachedVerify? /** * Verify signatures of an inline-signed message. If you need to verify detached signatures over * a message, use [detachedVerify] instead. + * + * @since sopv 1.0 */ fun inlineVerify(): InlineVerify? - /** Validate a UserID in an OpenPGP certificate. */ + /** + * Validate a UserID in an OpenPGP certificate. + * + * @since sopv 1.2 + */ fun validateUserId(): ValidateUserId? } From cc08b76b68a66bc8ef094dcaccbee5c561bfabe0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Jul 2025 14:09:06 +0200 Subject: [PATCH 297/298] Introduce sop-java-json-gson module --- settings.gradle | 3 +- sop-java-json-gson/README.md | 13 +++ sop-java-json-gson/build.gradle | 28 ++++++ .../src/main/kotlin/sop/GsonParser.kt | 23 +++++ .../src/main/kotlin/sop/GsonSerializer.kt | 16 ++++ .../kotlin/sop/GsonSerializerAndParserTest.kt | 96 +++++++++++++++++++ 6 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 sop-java-json-gson/README.md create mode 100644 sop-java-json-gson/build.gradle create mode 100644 sop-java-json-gson/src/main/kotlin/sop/GsonParser.kt create mode 100644 sop-java-json-gson/src/main/kotlin/sop/GsonSerializer.kt create mode 100644 sop-java-json-gson/src/test/kotlin/sop/GsonSerializerAndParserTest.kt diff --git a/settings.gradle b/settings.gradle index 1cb66be..84dc381 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,5 +7,6 @@ rootProject.name = 'SOP-Java' include 'sop-java', 'sop-java-picocli', 'sop-java-testfixtures', - 'external-sop' + 'external-sop', + 'sop-java-json-gson' diff --git a/sop-java-json-gson/README.md b/sop-java-json-gson/README.md new file mode 100644 index 0000000..9feb8ff --- /dev/null +++ b/sop-java-json-gson/README.md @@ -0,0 +1,13 @@ + + +# SOP-Java-JSON-GSON + +## JSON Parsing VERIFICATION extension JSON using Gson + +Since revision 11, the SOP specification defines VERIFICATIONS extension JSON. + +This module implements the `JSONParser` and `JSONSerializer` interfaces using Googles Gson library. diff --git a/sop-java-json-gson/build.gradle b/sop-java-json-gson/build.gradle new file mode 100644 index 0000000..4105902 --- /dev/null +++ b/sop-java-json-gson/build.gradle @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +plugins { + id 'java-library' +} + +group 'org.pgpainless' + +repositories { + mavenCentral() +} + +dependencies { + implementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + implementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" + runtimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + implementation project(":sop-java") + api "org.slf4j:slf4j-api:$slf4jVersion" + testImplementation "ch.qos.logback:logback-classic:$logbackVersion" + + // @Nonnull, @Nullable... + implementation "com.google.code.findbugs:jsr305:$jsrVersion" + + api "com.google.code.gson:gson:$gsonVersion" +} diff --git a/sop-java-json-gson/src/main/kotlin/sop/GsonParser.kt b/sop-java-json-gson/src/main/kotlin/sop/GsonParser.kt new file mode 100644 index 0000000..06adecb --- /dev/null +++ b/sop-java-json-gson/src/main/kotlin/sop/GsonParser.kt @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import com.google.gson.reflect.TypeToken +import java.text.ParseException + +class GsonParser( + private val gson: Gson = Gson() +) : Verification.JSONParser { + + override fun parse(string: String): Verification.JSON { + try { + return gson.fromJson(string, object : TypeToken(){}.type) + } catch (e: JsonSyntaxException) { + throw ParseException(e.message, 0) + } + } +} \ No newline at end of file diff --git a/sop-java-json-gson/src/main/kotlin/sop/GsonSerializer.kt b/sop-java-json-gson/src/main/kotlin/sop/GsonSerializer.kt new file mode 100644 index 0000000..410fe49 --- /dev/null +++ b/sop-java-json-gson/src/main/kotlin/sop/GsonSerializer.kt @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import com.google.gson.Gson + +class GsonSerializer( + private val gson: Gson = Gson() +) : Verification.JSONSerializer { + + override fun serialize(json: Verification.JSON): String { + return gson.toJson(json) + } +} \ No newline at end of file diff --git a/sop-java-json-gson/src/test/kotlin/sop/GsonSerializerAndParserTest.kt b/sop-java-json-gson/src/test/kotlin/sop/GsonSerializerAndParserTest.kt new file mode 100644 index 0000000..9bbef14 --- /dev/null +++ b/sop-java-json-gson/src/test/kotlin/sop/GsonSerializerAndParserTest.kt @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.text.ParseException + +class GsonSerializerAndParserTest { + + private val serializer: GsonSerializer = GsonSerializer() + private val parser: GsonParser = GsonParser() + + @Test + fun simpleSingleTest() { + val before = Verification.JSON("/tmp/alice.pgp") + + val json = serializer.serialize(before) + assertEquals("{\"signers\":[\"/tmp/alice.pgp\"]}", json) + + val after = parser.parse(json) + + assertEquals(before, after) + } + + @Test + fun simpleListTest() { + val before = Verification.JSON(listOf("/tmp/alice.pgp", "/tmp/bob.asc")) + + val json = serializer.serialize(before) + assertEquals("{\"signers\":[\"/tmp/alice.pgp\",\"/tmp/bob.asc\"]}", json) + + val after = parser.parse(json) + + assertEquals(before, after) + } + + @Test + fun withCommentTest() { + val before = Verification.JSON( + listOf("/tmp/alice.pgp"), + "This is a comment.", + null) + + val json = serializer.serialize(before) + assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\"}", json) + + val after = parser.parse(json) + + assertEquals(before, after) + } + + @Test + fun withExtStringTest() { + val before = Verification.JSON( + listOf("/tmp/alice.pgp"), + "This is a comment.", + "This is an ext object string.") + + val json = serializer.serialize(before) + assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\",\"ext\":\"This is an ext object string.\"}", json) + + val after = parser.parse(json) + + assertEquals(before, after) + } + + @Test + fun withExtListTest() { + val before = Verification.JSON( + listOf("/tmp/alice.pgp"), + "This is a comment.", + listOf(1.0,2.0,3.0)) + + val json = serializer.serialize(before) + assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\",\"ext\":[1.0,2.0,3.0]}", json) + + val after = parser.parse(json) + + assertEquals(before, after) + } + + @Test + fun parseInvalidJSON() { + assertThrows { parser.parse("Invalid") } + } + + @Test + fun parseMalformedJSON() { + // Missing '}' + assertThrows { parser.parse("{\"signers\":[\"Alice\"]") } + } +} \ No newline at end of file From b28e234f2124f36a549eab238b1070390ac55dfc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Jul 2025 14:09:57 +0200 Subject: [PATCH 298/298] ExternalTestSuite: Add license header --- .../test/java/sop/testsuite/external/ExternalTestSuite.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java b/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java index 6e991cf..aa0ae82 100644 --- a/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java +++ b/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package sop.testsuite.external; import org.junit.platform.suite.api.IncludeClassNamePatterns;