diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 4f92d0e..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -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** - diff --git a/.reuse/dep5 b/.reuse/dep5 new file mode 100644 index 0000000..8feb5a2 --- /dev/null +++ b/.reuse/dep5 @@ -0,0 +1,24 @@ +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 diff --git a/.woodpecker/build.yml b/.woodpecker/.build.yml similarity index 75% rename from .woodpecker/build.yml rename to .woodpecker/.build.yml index 4c23ffb..d72d19e 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/.build.yml @@ -1,8 +1,6 @@ -steps: +pipeline: run: - when: - event: push - image: gradle:7.6-jdk11-jammy + image: gradle:7.5-jdk8-jammy commands: # Install Sequoia-SOP - apt update && apt install --yes sqop @@ -16,6 +14,4 @@ steps: - gradle check javadocAll # Code has coverage - gradle jacocoRootReport coveralls - environment: - COVERALLS_REPO_TOKEN: - from_secret: coveralls_repo_token + secrets: [COVERALLS_REPO_TOKEN] diff --git a/.woodpecker/reuse.yml b/.woodpecker/.reuse.yml similarity index 67% rename from .woodpecker/reuse.yml rename to .woodpecker/.reuse.yml index b278a39..58f17e6 100644 --- a/.woodpecker/reuse.yml +++ b/.woodpecker/.reuse.yml @@ -1,9 +1,7 @@ # Code is licensed properly # See https://reuse.software/ -steps: +pipeline: reuse: - when: - event: push image: fsfe/reuse:latest commands: - - reuse lint + - reuse lint \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0447a9a..94f2f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,89 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 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 - - `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` - -## 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 -- gradle: Improve reproducibility -- gradle: Bump animalsniffer to `2.0.0` - -## 10.1.0 -- `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 - -## 10.0.2 -- 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 -- 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 -- 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). - - Throw `BadData` when passing KEYS where CERTS are expected - - 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. - -## 8.0.0 -- 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 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`. - - `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.2 +## 7.0.2-SNAPSHOT - 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 diff --git a/README.md b/README.md index 35324c4..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: 14](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/14/) +[![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) @@ -25,10 +25,6 @@ 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. -* [sop-java-testfixtures](/sop-java-testfixtures) contains a test suite that can be shared by downstream implementations - of `sop-java`. ## Known Implementations (Please expand!) @@ -37,11 +33,9 @@ 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 | -|---------------------------------------------------|----------| -| [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 | +| Project | Language | +|-------------------------------------------------|----------| +| [sop-rs](https://sequoia-pgp.gitlab.io/sop-rs/) | Rust | +| [SOP for python](https://pypi.org/project/sop/) | Python | diff --git a/REUSE.toml b/REUSE.toml deleted file mode 100644 index 7e1b250..0000000 --- a/REUSE.toml +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Paul Schaub -# -# SPDX-License-Identifier: CC0-1.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" diff --git a/build.gradle b/build.gradle index 10f2b87..56730fd 100644 --- a/build.gradle +++ b/build.gradle @@ -18,8 +18,7 @@ buildscript { } plugins { - id 'org.jetbrains.kotlin.jvm' version "1.9.21" - id 'com.diffplug.spotless' version '6.22.0' apply false + id 'ru.vyarus.animalsniffer' version '1.5.3' } apply from: 'version.gradle' @@ -30,9 +29,18 @@ allprojects { apply plugin: 'eclipse' 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 + 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 @@ -45,16 +53,12 @@ allprojects { toolVersion = '8.18' } - spotless { - kotlin { - ktfmt().dropboxStyle() - } - } - group 'org.pgpainless' description = "Stateless OpenPGP Protocol API for Java" version = shortVersion + sourceCompatibility = javaSourceCompatibility + repositories { mavenCentral() } @@ -63,20 +67,6 @@ allprojects { tasks.withType(AbstractArchiveTask) { preserveFileTimestamps = false reproducibleFileOrder = true - - dirMode = 0755 - 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 { - freeCompilerArgs += ["-Xjvm-default=all-compatibility"] - } } project.ext { @@ -104,7 +94,7 @@ allprojects { } jacoco { - toolVersion = "0.8.8" + toolVersion = "0.8.7" } jacocoTestReport { @@ -112,7 +102,7 @@ allprojects { sourceDirectories.setFrom(project.files(sourceSets.main.allSource.srcDirs)) classDirectories.setFrom(project.files(sourceSets.main.output)) reports { - xml.required = true + xml.enabled true } } @@ -130,15 +120,15 @@ subprojects { apply plugin: 'signing' task sourcesJar(type: Jar, dependsOn: classes) { - archiveClassifier = 'sources' + classifier = 'sources' from sourceSets.main.allSource } task javadocJar(type: Jar, dependsOn: javadoc) { - archiveClassifier = 'javadoc' + classifier = 'javadoc' from javadoc.destinationDir } task testsJar(type: Jar, dependsOn: testClasses) { - archiveClassifier = 'tests' + classifier = 'tests' from sourceSets.test.output } @@ -235,7 +225,7 @@ task jacocoRootReport(type: JacocoReport) { classDirectories.setFrom(files(subprojects.sourceSets.main.output)) executionData.setFrom(files(subprojects.jacocoTestReport.executionData)) reports { - xml.required = true + xml.enabled true xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml") } // We could remove the following setOnlyIf line, but then @@ -246,6 +236,10 @@ 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/external-sop/build.gradle b/external-sop/build.gradle index 2dfbf7e..1bb86fc 100644 --- a/external-sop/build.gradle +++ b/external-sop/build.gradle @@ -15,9 +15,7 @@ repositories { dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" - - testImplementation "org.junit.platform:junit-platform-suite-api:1.13.2" - testRuntimeOnly 'org.junit.platform:junit-platform-suite:1.13.2' + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" api project(":sop-java") api "org.slf4j:slf4j-api:$slf4jVersion" @@ -29,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(project(":sop-java-testfixtures")) + testImplementation(testFixtures(project(":sop-java"))) } test { diff --git a/external-sop/src/main/java/sop/external/ExternalSOP.java b/external-sop/src/main/java/sop/external/ExternalSOP.java new file mode 100644 index 0000000..376e54c --- /dev/null +++ b/external-sop/src/main/java/sop/external/ExternalSOP.java @@ -0,0 +1,454 @@ +// 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 + public Version version() { + return new VersionExternal(binaryName, properties); + } + + @Override + public GenerateKey generateKey() { + return new GenerateKeyExternal(binaryName, properties); + } + + @Override + public ExtractCert extractCert() { + return new ExtractCertExternal(binaryName, properties); + } + + @Override + public DetachedSign detachedSign() { + return new DetachedSignExternal(binaryName, properties, tempDirProvider); + } + + @Override + public InlineSign inlineSign() { + return new InlineSignExternal(binaryName, properties); + } + + @Override + public DetachedVerify detachedVerify() { + return new DetachedVerifyExternal(binaryName, properties); + } + + @Override + public InlineVerify inlineVerify() { + return new InlineVerifyExternal(binaryName, properties, tempDirProvider); + } + + @Override + public InlineDetach inlineDetach() { + return new InlineDetachExternal(binaryName, properties, tempDirProvider); + } + + @Override + public Encrypt encrypt() { + return new EncryptExternal(binaryName, properties); + } + + @Override + public Decrypt decrypt() { + return new DecryptExternal(binaryName, properties, tempDirProvider); + } + + @Override + public Armor armor() { + return new ArmorExternal(binaryName, properties); + } + + @Override + public ListProfiles listProfiles() { + return new ListProfilesExternal(binaryName, properties); + } + + @Override + public RevokeKey revokeKey() { + return new RevokeKeyExternal(binaryName, properties); + } + + @Override + public ChangeKeyPassword changeKeyPassword() { + return new ChangeKeyPasswordExternal(binaryName, properties); + } + + @Override + 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(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(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/java/sop/external/operation/ArmorExternal.java b/external-sop/src/main/java/sop/external/operation/ArmorExternal.java new file mode 100644 index 0000000..4df7fca --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/ArmorExternal.java @@ -0,0 +1,42 @@ +// 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 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 + public Armor label(ArmorLabel label) throws SOPGPException.UnsupportedOption { + commandList.add("--label=" + label); + return this; + } + + @Override + 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/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/main/java/sop/external/operation/DearmorExternal.java b/external-sop/src/main/java/sop/external/operation/DearmorExternal.java new file mode 100644 index 0000000..bedf018 --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/DearmorExternal.java @@ -0,0 +1,35 @@ +// 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 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 + 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/DecryptExternal.java b/external-sop/src/main/java/sop/external/operation/DecryptExternal.java new file mode 100644 index 0000000..0b91d5b --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/DecryptExternal.java @@ -0,0 +1,176 @@ +// 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 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 + public Decrypt verifyNotBefore(Date timestamp) + throws SOPGPException.UnsupportedOption { + this.commandList.add("--verify-not-before=" + UTCUtil.formatUTCDate(timestamp)); + return this; + } + + @Override + public Decrypt verifyNotAfter(Date timestamp) + throws SOPGPException.UnsupportedOption { + this.commandList.add("--verify-not-after=" + UTCUtil.formatUTCDate(timestamp)); + return this; + } + + @Override + public Decrypt verifyWithCert(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 + public Decrypt withSessionKey(SessionKey sessionKey) + throws SOPGPException.UnsupportedOption { + String envVar = "SESSION_KEY_" + withSessionKeyCounter++; + commandList.add("--with-session-key=@ENV:" + envVar); + envList.add(envVar + "=" + sessionKey); + return this; + } + + @Override + public Decrypt withPassword(String password) + throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { + String envVar = "PASSWORD_" + withPasswordCounter++; + commandList.add("--with-password=@ENV:" + envVar); + envList.add(envVar + "=" + password); + return this; + } + + @Override + public Decrypt withKey(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 + public Decrypt 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 ReadyWithResult ciphertext(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(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/java/sop/external/operation/DetachedSignExternal.java b/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java new file mode 100644 index 0000000..7c579d4 --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java @@ -0,0 +1,136 @@ +// 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 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 + public DetachedSign noArmor() { + commandList.add("--no-armor"); + return this; + } + + @Override + public DetachedSign key(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 + public DetachedSign withKeyPassword(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 + public DetachedSign mode(SignAs mode) throws SOPGPException.UnsupportedOption { + commandList.add("--as=" + mode); + return this; + } + + @Override + public ReadyWithResult data(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(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/java/sop/external/operation/DetachedVerifyExternal.java b/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java new file mode 100644 index 0000000..2f19c5b --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java @@ -0,0 +1,111 @@ +// 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 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 + public DetachedVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { + commandList.add("--not-before=" + UTCUtil.formatUTCDate(timestamp)); + return this; + } + + @Override + public DetachedVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { + commandList.add("--not-after=" + UTCUtil.formatUTCDate(timestamp)); + return this; + } + + @Override + public DetachedVerify cert(InputStream cert) throws SOPGPException.BadData { + this.certs.add(cert); + return this; + } + + @Override + public VerifySignatures signatures(InputStream signatures) throws SOPGPException.BadData { + this.signatures = signatures; + return this; + } + + @Override + public List data(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/java/sop/external/operation/EncryptExternal.java b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java new file mode 100644 index 0000000..bc40208 --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation; + +import sop.Ready; +import sop.enums.EncryptAs; +import sop.exception.SOPGPException; +import sop.external.ExternalSOP; +import sop.operation.Encrypt; + +import java.io.IOException; +import java.io.InputStream; +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 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) { + this.commandList.add(binary); + this.commandList.add("encrypt"); + this.envList = ExternalSOP.propertiesToEnv(environment); + } + + @Override + public Encrypt noArmor() { + this.commandList.add("--no-armor"); + return this; + } + + @Override + public Encrypt mode(EncryptAs mode) + throws SOPGPException.UnsupportedOption { + this.commandList.add("--as=" + mode); + return this; + } + + @Override + public Encrypt signWith(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 + public Encrypt withKeyPassword(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 + public Encrypt withPassword(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 + public Encrypt withCert(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 + public Encrypt profile(String profileName) { + commandList.add("--profile=" + profileName); + return this; + } + + @Override + public Ready plaintext(InputStream plaintext) + throws SOPGPException.KeyIsProtected { + return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, plaintext); + } +} diff --git a/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java b/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java new file mode 100644 index 0000000..5fdcdc1 --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java @@ -0,0 +1,41 @@ +// 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 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 + public ExtractCert noArmor() { + this.commandList.add("--no-armor"); + return this; + } + + @Override + public Ready key(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 new file mode 100644 index 0000000..c46dfb3 --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java @@ -0,0 +1,71 @@ +// 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 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 + public GenerateKey noArmor() { + this.commandList.add("--no-armor"); + return this; + } + + @Override + public GenerateKey userId(String userId) { + this.commandList.add(userId); + return this; + } + + @Override + public GenerateKey withKeyPassword(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 + public GenerateKey profile(String profile) { + commandList.add("--profile=" + profile); + return this; + } + + @Override + public GenerateKey signingOnly() { + commandList.add("--signing-only"); + return this; + } + + @Override + 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 new file mode 100644 index 0000000..c03fe1b --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java @@ -0,0 +1,103 @@ +// 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 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 + public InlineDetach noArmor() { + commandList.add("--no-armor"); + return this; + } + + @Override + public ReadyWithResult message(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(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(OutputStream signatureOutputStream) throws IOException { + signatureOutputStream.write(sigBytes); + } + }; + } + }; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java b/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java new file mode 100644 index 0000000..68a630d --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java @@ -0,0 +1,68 @@ +// 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 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 + public InlineSign noArmor() { + commandList.add("--no-armor"); + return this; + } + + @Override + public InlineSign key(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 + public InlineSign withKeyPassword(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 + public InlineSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption { + commandList.add("--as=" + mode); + return this; + } + + @Override + public Ready data(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 new file mode 100644 index 0000000..8010367 --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java @@ -0,0 +1,117 @@ +// 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 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 + public InlineVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { + commandList.add("--not-before=" + UTCUtil.formatUTCDate(timestamp)); + return this; + } + + @Override + public InlineVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { + commandList.add("--not-after=" + UTCUtil.formatUTCDate(timestamp)); + return this; + } + + @Override + public InlineVerify cert(InputStream cert) throws SOPGPException.BadData, IOException { + String envVar = "CERT_" + certCounter++; + commandList.add("@ENV:" + envVar); + envList.add(envVar + "=" + ExternalSOP.readString(cert)); + return this; + } + + @Override + public ReadyWithResult> data(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(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/java/sop/external/operation/ListProfilesExternal.java b/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java new file mode 100644 index 0000000..0c76b63 --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java @@ -0,0 +1,48 @@ +// 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 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 + public List subcommand(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/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/main/java/sop/external/operation/VersionExternal.java b/external-sop/src/main/java/sop/external/operation/VersionExternal.java new file mode 100644 index 0000000..0b9c5b4 --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/VersionExternal.java @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation; + +import sop.external.ExternalSOP; +import sop.operation.Version; + +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 + 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 + 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 + 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 + 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 + 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/src/main/kotlin/sop/enums/EncryptAs.kt b/external-sop/src/main/java/sop/external/operation/package-info.java similarity index 55% rename from sop-java/src/main/kotlin/sop/enums/EncryptAs.kt rename to external-sop/src/main/java/sop/external/operation/package-info.java index 0b6aa8e..9c7dd29 100644 --- a/sop-java/src/main/kotlin/sop/enums/EncryptAs.kt +++ b/external-sop/src/main/java/sop/external/operation/package-info.java @@ -2,9 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package sop.enums - -enum class EncryptAs { - binary, - text -} +/** + * 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 new file mode 100644 index 0000000..208985e --- /dev/null +++ b/external-sop/src/main/java/sop/external/package-info.java @@ -0,0 +1,8 @@ +// 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/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt deleted file mode 100644 index 48a5af9..0000000 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt +++ /dev/null @@ -1,344 +0,0 @@ -// 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 [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) - - 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 - * 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) { - 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") - 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") - NoHardwareKeyFound.EXIT_CODE -> - throw NoHardwareKeyFound( - "External SOP backend reported error NoHardwareKeyFound ($exitCode):\n$errorMessage") - HardwareKeyFailure.EXIT_CODE -> - throw HardwareKeyFailure( - "External SOP backend reported error HardwareKeyFailure ($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") - 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 -> - 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() } - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt deleted file mode 100644 index 3341055..0000000 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt +++ /dev/null @@ -1,57 +0,0 @@ -// 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.ValidateUserIdExternal -import sop.external.operation.VersionExternal -import sop.operation.DetachedVerify -import sop.operation.InlineVerify -import sop.operation.ValidateUserId -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) - - override fun validateUserId(): ValidateUserId = ValidateUserIdExternal(binaryName, properties) - - 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/ArmorExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt deleted file mode 100644 index b202746..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt +++ /dev/null @@ -1,23 +0,0 @@ -// 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.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) - - @Throws(SOPGPException.BadData::class) - override fun data(data: InputStream): Ready = - ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt deleted file mode 100644 index e3661db..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt +++ /dev/null @@ -1,48 +0,0 @@ -// 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, "certify-userid") - 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("--").plus(keys), envList, certs) -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/ChangeKeyPasswordExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ChangeKeyPasswordExternal.kt deleted file mode 100644 index a45e59f..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/ChangeKeyPasswordExternal.kt +++ /dev/null @@ -1,37 +0,0 @@ -// 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) -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt deleted file mode 100644 index 928d9b4..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt +++ /dev/null @@ -1,20 +0,0 @@ -// 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) -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt deleted file mode 100644 index 1e6d6a2..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt +++ /dev/null @@ -1,133 +0,0 @@ -// 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("--verifications-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: String? = readString(sessionKeyOutIn) - val sessionKey = line?.let { l -> SessionKey.fromString(l.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) { - line?.let { verifications.add(Verification.fromString(it.trim())) } - } - reader.close() - } - - return DecryptionResult(sessionKey, verifications) - } - } - } catch (e: IOException) { - throw 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 deleted file mode 100644 index 66d1db8..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/DetachedSignExternal.kt +++ /dev/null @@ -1,104 +0,0 @@ -// 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 - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt deleted file mode 100644 index 3340a33..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt +++ /dev/null @@ -1,90 +0,0 @@ -// 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) - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt deleted file mode 100644 index 679e09b..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt +++ /dev/null @@ -1,111 +0,0 @@ -// 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=${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) - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/ExtractCertExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ExtractCertExternal.kt deleted file mode 100644 index 9b86733..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/ExtractCertExternal.kt +++ /dev/null @@ -1,24 +0,0 @@ -// 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) -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt deleted file mode 100644 index 37116a4..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt +++ /dev/null @@ -1,38 +0,0 @@ -// 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) -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt deleted file mode 100644 index f44e6bb..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt +++ /dev/null @@ -1,82 +0,0 @@ -// 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) - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt deleted file mode 100644 index a304e85..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt +++ /dev/null @@ -1,40 +0,0 @@ -// 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 - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt deleted file mode 100644 index bf0c66b..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt +++ /dev/null @@ -1,91 +0,0 @@ -// 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 - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt deleted file mode 100644 index 5e8ff89..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt +++ /dev/null @@ -1,36 +0,0 @@ -// 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) } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt deleted file mode 100644 index b739eb3..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt +++ /dev/null @@ -1,30 +0,0 @@ -// 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, "merge-certs") - 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/RevokeKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/RevokeKeyExternal.kt deleted file mode 100644 index 43795e6..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/RevokeKeyExternal.kt +++ /dev/null @@ -1,31 +0,0 @@ -// 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) -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt deleted file mode 100644 index b84f452..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt +++ /dev/null @@ -1,43 +0,0 @@ -// 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 noAddedCapabilities(): UpdateKey = apply { - commandList.add("--no-added-capabilities") - } - - 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 deleted file mode 100644 index cf4742b..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt +++ /dev/null @@ -1,43 +0,0 @@ -// 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 -import sop.util.UTCUtil - -class ValidateUserIdExternal(binary: String, environment: Properties) : ValidateUserId { - - private val commandList = mutableListOf(binary, "validate-userid") - 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 - } - - override fun validateAt(date: Date): ValidateUserId = apply { - commandList.add("--validate-at=${UTCUtil.formatUTCDate(date)}") - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt deleted file mode 100644 index 728f3b6..0000000 --- a/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt +++ /dev/null @@ -1,102 +0,0 @@ -// 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 getSopVVersion(): String { - return executeForLines(commandList.plus("--sopv")) - } - - 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) - } - } -} diff --git a/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java b/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java deleted file mode 100644 index aa0ae82..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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 new file mode 100644 index 0000000..1d8ff2b --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalArmorDearmorTest.java @@ -0,0 +1,13 @@ +// 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/ExternalDecryptWithSessionKeyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalDecryptWithSessionKeyTest.java new file mode 100644 index 0000000..0ac03a4 --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalDecryptWithSessionKeyTest.java @@ -0,0 +1,13 @@ +// 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 new file mode 100644 index 0000000..13959df --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalDetachedSignDetachedVerifyTest.java @@ -0,0 +1,12 @@ +// 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 new file mode 100644 index 0000000..b83ca46 --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalEncryptDecryptTest.java @@ -0,0 +1,13 @@ +// 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 new file mode 100644 index 0000000..f47656c --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalExtractCertTest.java @@ -0,0 +1,13 @@ +// 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 new file mode 100644 index 0000000..7ac971b --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalGenerateKeyTest.java @@ -0,0 +1,13 @@ +// 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 new file mode 100644 index 0000000..2dd3396 --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineDetachDetachedVerifyTest.java @@ -0,0 +1,14 @@ +// 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 new file mode 100644 index 0000000..24e30aa --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineVerifyTest.java @@ -0,0 +1,13 @@ +// 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 new file mode 100644 index 0000000..18da883 --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalListProfilesTest.java @@ -0,0 +1,13 @@ +// 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/ExternalRevokeKeyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java new file mode 100644 index 0000000..e2efe03 --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java @@ -0,0 +1,13 @@ +// 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 new file mode 100644 index 0000000..ee63f09 --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalVersionTest.java @@ -0,0 +1,13 @@ +// 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 { + +} diff --git a/settings.gradle b/settings.gradle index 84dc381..5dc6372 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,5 @@ rootProject.name = 'SOP-Java' include 'sop-java', 'sop-java-picocli', - 'sop-java-testfixtures', - 'external-sop', - 'sop-java-json-gson' + 'external-sop' diff --git a/sop-java-json-gson/README.md b/sop-java-json-gson/README.md deleted file mode 100644 index 9feb8ff..0000000 --- a/sop-java-json-gson/README.md +++ /dev/null @@ -1,13 +0,0 @@ - - -# 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 deleted file mode 100644 index 4105902..0000000 --- a/sop-java-json-gson/build.gradle +++ /dev/null @@ -1,28 +0,0 @@ -// 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 deleted file mode 100644 index 06adecb..0000000 --- a/sop-java-json-gson/src/main/kotlin/sop/GsonParser.kt +++ /dev/null @@ -1,23 +0,0 @@ -// 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 deleted file mode 100644 index 410fe49..0000000 --- a/sop-java-json-gson/src/main/kotlin/sop/GsonSerializer.kt +++ /dev/null @@ -1,16 +0,0 @@ -// 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 deleted file mode 100644 index 9bbef14..0000000 --- a/sop-java-json-gson/src/test/kotlin/sop/GsonSerializerAndParserTest.kt +++ /dev/null @@ -1,96 +0,0 @@ -// 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 diff --git a/sop-java-picocli/build.gradle b/sop-java-picocli/build.gradle index 2203abe..664c385 100644 --- a/sop-java-picocli/build.gradle +++ b/sop-java-picocli/build.gradle @@ -12,16 +12,19 @@ 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(project(":sop-java-testfixtures")) // CLI implementation "info.picocli:picocli:$picocliVersion" - kapt "info.picocli:picocli-codegen:$picocliVersion" + annotationProcessor "info.picocli:picocli-codegen:$picocliVersion" // @Nonnull, @Nullable... implementation "com.google.code.findbugs:jsr305:$jsrVersion" @@ -33,13 +36,8 @@ application { mainClass = mainClassName } -compileJava { - options.compilerArgs += ["-Aproject=${project.group}/${project.name}"] -} - jar { dependsOn(":sop-java:jar") - duplicatesStrategy(DuplicatesStrategy.EXCLUDE) manifest { attributes 'Main-Class': "$mainClassName" 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 new file mode 100644 index 0000000..9e81b66 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java @@ -0,0 +1,14 @@ +// 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 + } +} 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 new file mode 100644 index 0000000..8b38af3 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java @@ -0,0 +1,34 @@ +// 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/java/sop/cli/picocli/SOPExecutionExceptionHandler.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java new file mode 100644 index 0000000..f6906ff --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java @@ -0,0 +1,33 @@ +// 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/java/sop/cli/picocli/SopCLI.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java new file mode 100644 index 0000000..5420dea --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java @@ -0,0 +1,129 @@ +// 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/commands/AbstractSopCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java new file mode 100644 index 0000000..9aec5a7 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java @@ -0,0 +1,282 @@ +// 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/java/sop/cli/picocli/commands/ArmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java new file mode 100644 index 0000000..5691686 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java @@ -0,0 +1,50 @@ +// 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/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..0e12ef8 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java @@ -0,0 +1,58 @@ +// 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(); + } + + try { + for (String oldKeyPassword : oldKeyPasswords) { + String password = stringFromInputStream(getInput(oldKeyPassword)); + changeKeyPassword.oldKeyPassphrase(password); + } + + if (newKeyPassword != null) { + String password = stringFromInputStream(getInput(newKeyPassword)); + changeKeyPassword.newKeyPassphrase(password); + } + + changeKeyPassword.keys(System.in).writeTo(System.out); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} 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 new file mode 100644 index 0000000..f73e351 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java @@ -0,0 +1,47 @@ +// 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/java/sop/cli/picocli/commands/DecryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java new file mode 100644 index 0000000..a870931 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java @@ -0,0 +1,250 @@ +// 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) { + 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/java/sop/cli/picocli/commands/EncryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java new file mode 100644 index 0000000..efda26f --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java @@ -0,0 +1,154 @@ +// 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/java/sop/cli/picocli/commands/ExtractCertCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java new file mode 100644 index 0000000..64a7a84 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java @@ -0,0 +1,43 @@ +// 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/java/sop/cli/picocli/commands/GenerateKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java new file mode 100644 index 0000000..eea992e --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java @@ -0,0 +1,85 @@ +// 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/java/sop/cli/picocli/commands/InlineDetachCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java new file mode 100644 index 0000000..52b654f --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java @@ -0,0 +1,50 @@ +// 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/java/sop/cli/picocli/commands/InlineSignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java new file mode 100644 index 0000000..1865bcf --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java @@ -0,0 +1,101 @@ +// 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/java/sop/cli/picocli/commands/InlineVerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java new file mode 100644 index 0000000..c413c85 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java @@ -0,0 +1,108 @@ +// 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/ListProfilesCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java new file mode 100644 index 0000000..53ec024 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java @@ -0,0 +1,36 @@ +// 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/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..3c2e45b --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java @@ -0,0 +1,66 @@ +// 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; +import java.util.ArrayList; +import java.util.List; + +@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") + List withKeyPassword = new ArrayList<>(); + + @Override + public void run() { + RevokeKey revokeKey = throwIfUnsupportedSubcommand( + SopCLI.getSop().revokeKey(), "revoke-key"); + + if (!armor) { + revokeKey.noArmor(); + } + + if (withKeyPassword != null) { + try { + for (String passwordFile : withKeyPassword) { + String password = stringFromInputStream(getInput(passwordFile)); + 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/java/sop/cli/picocli/commands/SignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java new file mode 100644 index 0000000..cad9d6e --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java @@ -0,0 +1,108 @@ +// 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/java/sop/cli/picocli/commands/VerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java new file mode 100644 index 0000000..d76bb37 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java @@ -0,0 +1,102 @@ +// 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/java/sop/cli/picocli/commands/VersionCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java new file mode 100644 index 0000000..6ccb8f7 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java @@ -0,0 +1,58 @@ +// 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/java/sop/cli/picocli/commands/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java new file mode 100644 index 0000000..fc6aefd --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java @@ -0,0 +1,8 @@ +// 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/java/sop/cli/picocli/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java new file mode 100644 index 0000000..83f426d --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java @@ -0,0 +1,8 @@ +// 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/SOPExceptionExitCodeMapper.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt deleted file mode 100644 index 5778bb9..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt +++ /dev/null @@ -1,33 +0,0 @@ -// 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 MissingParameterException) { - SOPGPException.MissingArg.EXIT_CODE - } else if (exception is ParameterException) { - // Invalid option (e.g. `--as invalid`) - SOPGPException.UnsupportedOption.EXIT_CODE - } else { - // Others, like IOException etc. - 1 - } -} 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 deleted file mode 100644 index 52236d3..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExecutionExceptionHandler.kt +++ /dev/null @@ -1,34 +0,0 @@ -// 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 - } -} 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 deleted file mode 100644 index 07caa03..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ /dev/null @@ -1,121 +0,0 @@ -// 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, - UpdateKeyCmd::class, - MergeCertsCmd::class, - CertifyUserIdCmd::class, - ValidateUserIdCmd::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", "--debug"], scope = 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()).setUnmatchedArgumentsAllowed(true).parseArgs(*args) - - // Re-set bundle with updated locale - cliMsg = ResourceBundle.getBundle("msg_sop") - - return CommandLine(SopCLI::class.java) - .apply { - // 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) - } - } - - /** - * 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/main/kotlin/sop/cli/picocli/SopVCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt deleted file mode 100644 index 311a446..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt +++ /dev/null @@ -1,99 +0,0 @@ -// 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", "--debug"], 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/AbstractSopCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt deleted file mode 100644 index 65be1be..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt +++ /dev/null @@ -1,348 +0,0 @@ -// 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 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 -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) - } - } - } - - /** - * 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 - @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/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt deleted file mode 100644 index 50716f1..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt +++ /dev/null @@ -1,32 +0,0 @@ -// 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 sop.cli.picocli.SopCLI -import sop.exception.SOPGPException.BadData -import sop.exception.SOPGPException.UnsupportedOption - -@Command( - name = "armor", - resourceBundle = "msg_armor", - exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) -class ArmorCmd : AbstractSopCmd() { - - override fun run() { - val armor = throwIfUnsupportedSubcommand(SopCLI.getSop().armor(), "armor") - - 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) - } - } -} 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 deleted file mode 100644 index 228809b..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt +++ /dev/null @@ -1,84 +0,0 @@ -// 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.BadData -import sop.exception.SOPGPException.UnsupportedOption - -@Command( - name = "certify-userid", - resourceBundle = "msg_certify-userid", - exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE, - showEndOfOptionsDelimiterInUsageHelp = true) -class CertifyUserIdCmd : AbstractSopCmd() { - - @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/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt deleted file mode 100644 index be37309..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt +++ /dev/null @@ -1,52 +0,0 @@ -// 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 { - val password = stringFromInputStream(getInput(it)) - changeKeyPassword.oldKeyPassphrase(password) - } - - newKeyPassword?.let { - val password = stringFromInputStream(getInput(it)) - changeKeyPassword.newKeyPassphrase(password) - } - - try { - changeKeyPassword.keys(System.`in`).writeTo(System.out) - } catch (e: IOException) { - throw 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 deleted file mode 100644 index 09d2a71..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt +++ /dev/null @@ -1,41 +0,0 @@ -// 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 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) - } - } -} 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 deleted file mode 100644 index de98f17..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt +++ /dev/null @@ -1,224 +0,0 @@ -// 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.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, "--verify-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 { - getOutput(it).use { out -> - PrintWriter(out).use { pw -> - result.verifications.forEach { verification -> pw.println(verification) } - } - } - } - } - - @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/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt deleted file mode 100644 index 856bc76..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt +++ /dev/null @@ -1,159 +0,0 @@ -// 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.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() - - @Option(names = ["--session-key-out"], paramLabel = "SESSIONKEY") - var sessionKeyOut: String? = null - - override fun run() { - val encrypt = throwIfUnsupportedSubcommand(SopCLI.getSop().encrypt(), "encrypt") - - throwIfOutputExists(sessionKeyOut) - - 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`) - val result = ready.writeTo(System.out) - - if (sessionKeyOut == null) { - return - } - - getOutput(sessionKeyOut).use { - if (!result.sessionKey.isPresent) { - val errorMsg = getMsg("sop.error.runtime.no_session_key_extracted") - throw UnsupportedOption(String.format(errorMsg, "--session-key-out")) - } - val sessionKey = result.sessionKey.get() ?: return - val writer = PrintWriter(it) - writer.println(sessionKey) - writer.flush() - } - } catch (e: IOException) { - throw RuntimeException(e) - } - } -} 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 deleted file mode 100644 index cff996f..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ExtractCertCmd.kt +++ /dev/null @@ -1,40 +0,0 @@ -// 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) - } - } -} 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 deleted file mode 100644 index 7fa5a70..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt +++ /dev/null @@ -1,76 +0,0 @@ -// 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) - } - } -} 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 deleted file mode 100644 index e311adf..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt +++ /dev/null @@ -1,47 +0,0 @@ -// 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) - } - } -} 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 deleted file mode 100644 index c41f6f6..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt +++ /dev/null @@ -1,89 +0,0 @@ -// 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) - } - } -} 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 deleted file mode 100644 index 6a641a6..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineVerifyCmd.kt +++ /dev/null @@ -1,93 +0,0 @@ -// 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) - } - } - } -} 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 deleted file mode 100644 index b770e82..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt +++ /dev/null @@ -1,34 +0,0 @@ -// 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) - } - } -} 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 deleted file mode 100644 index 3dcef38..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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 - -@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 = true - - @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) - } - } -} 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 deleted file mode 100644 index b9b1015..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt +++ /dev/null @@ -1,58 +0,0 @@ -// 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", arity = "0..*") - var withKeyPassword: List = listOf() - - override fun run() { - val revokeKey = throwIfUnsupportedSubcommand(SopCLI.getSop().revokeKey(), "revoke-key") - - if (!armor) { - revokeKey.noArmor() - } - - for (passwordIn in withKeyPassword) { - try { - val password = stringFromInputStream(getInput(passwordIn)) - 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) - } - } -} 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 deleted file mode 100644 index 6860477..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/SignCmd.kt +++ /dev/null @@ -1,90 +0,0 @@ -// 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) - } - } -} 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 deleted file mode 100644 index 931f241..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt +++ /dev/null @@ -1,79 +0,0 @@ -// 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 sop.cli.picocli.SopCLI -import sop.exception.SOPGPException.* - -@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-added-capabilities"]) var noAddedCapabilities = 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 (noAddedCapabilities) { - updateKey.noAddedCapabilities() - } - - 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 { updateKey.mergeCerts(it) } - } 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) - } 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/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt deleted file mode 100644 index b83e5a8..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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 - -@Command( - name = "validate-userid", - resourceBundle = "msg_validate-userid", - exitCodeOnInvalidInput = SOPGPException.MissingArg.EXIT_CODE, - showEndOfOptionsDelimiterInUsageHelp = true) -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") - var authorities: List = listOf() - - override fun run() { - val validateUserId = - throwIfUnsupportedSubcommand(SopCLI.getSop().validateUserId(), "validate-userid") - - if (addrSpecOnly) { - validateUserId.addrSpecOnly() - } - - if (validateAt != null) { - validateUserId.validateAt(validateAt!!) - } - - 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/kotlin/sop/cli/picocli/commands/VerifyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VerifyCmd.kt deleted file mode 100644 index ef27266..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VerifyCmd.kt +++ /dev/null @@ -1,81 +0,0 @@ -// 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()) - } - } -} 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 deleted file mode 100644 index 8b1936a..0000000 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt +++ /dev/null @@ -1,57 +0,0 @@ -// 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 - @Option(names = ["--sopv"]) var sopv: 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 - } - - if (exclusive!!.sopv) { - println(version.getSopVVersion()) - return - } - } -} diff --git a/sop-java-picocli/src/main/resources/msg_armor.properties b/sop-java-picocli/src/main/resources/msg_armor.properties index 1b7c1fb..2f4e217 100644 --- a/sop-java-picocli/src/main/resources/msg_armor.properties +++ b/sop-java-picocli/src/main/resources/msg_armor.properties @@ -2,14 +2,11 @@ # # 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 +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.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_armor_de.properties b/sop-java-picocli/src/main/resources/msg_armor_de.properties index 34383c8..a2303e9 100644 --- a/sop-java-picocli/src/main/resources/msg_armor_de.properties +++ b/sop-java-picocli/src/main/resources/msg_armor_de.properties @@ -2,12 +2,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 +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.optionListHeading=%nOptionen:%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_certify-userid.properties b/sop-java-picocli/src/main/resources/msg_certify-userid.properties deleted file mode 100644 index 36dc6f4..0000000 --- a/sop-java-picocli/src/main/resources/msg_certify-userid.properties +++ /dev/null @@ -1,25 +0,0 @@ -# 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 - -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 -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_certify-userid_de.properties b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties deleted file mode 100644 index d634c59..0000000 --- a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties +++ /dev/null @@ -1,22 +0,0 @@ -# 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 - -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 -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 79bc11b..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 @@ -12,15 +12,10 @@ 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 5515c1d..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 @@ -12,13 +12,10 @@ 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 55cbf45..b088de1 100644 --- a/sop-java-picocli/src/main/resources/msg_dearmor.properties +++ b/sop-java-picocli/src/main/resources/msg_dearmor.properties @@ -3,14 +3,9 @@ # 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 e01ab7a..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,12 +3,9 @@ # 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 bec315f..5903ded 100644 --- a/sop-java-picocli/src/main/resources/msg_decrypt.properties +++ b/sop-java-picocli/src/main/resources/msg_decrypt.properties @@ -22,15 +22,10 @@ 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 395a89f..ba40897 100644 --- a/sop-java-picocli/src/main/resources/msg_decrypt_de.properties +++ b/sop-java-picocli/src/main/resources/msg_decrypt_de.properties @@ -22,13 +22,10 @@ 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 6ebfd0b..83359a6 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-sign.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-sign.properties @@ -11,15 +11,10 @@ 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 39b59b5..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 @@ -11,13 +11,10 @@ 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 074a318..ee1a468 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-verify.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-verify.properties @@ -13,16 +13,11 @@ 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 e21ee2a..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 @@ -13,14 +13,11 @@ 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 7bbf593..c0f7f7d 100644 --- a/sop-java-picocli/src/main/resources/msg_encrypt.properties +++ b/sop-java-picocli/src/main/resources/msg_encrypt.properties @@ -12,15 +12,10 @@ 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 55b0338..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,13 +12,10 @@ 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 1d1dee4..82cac0f 100644 --- a/sop-java-picocli/src/main/resources/msg_extract-cert.properties +++ b/sop-java-picocli/src/main/resources/msg_extract-cert.properties @@ -5,15 +5,10 @@ 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 c92d31d..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 @@ -5,13 +5,10 @@ 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 c17f7f6..60ff4a4 100644 --- a/sop-java-picocli/src/main/resources/msg_generate-key.properties +++ b/sop-java-picocli/src/main/resources/msg_generate-key.properties @@ -9,13 +9,10 @@ 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 84db04d..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 @@ -9,12 +9,10 @@ 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 637c1d0..797cc79 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 8471188..beea45c 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 ca0ed6b..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,14 +5,9 @@ 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 84b8c47..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,12 +5,9 @@ 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 936b417..f5143bb 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-sign.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-sign.properties @@ -13,15 +13,10 @@ 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 f8fe906..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 @@ -13,13 +13,10 @@ 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 2e0d69f..dfa94d7 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-verify.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-verify.properties @@ -12,15 +12,10 @@ 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 9b70504..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 @@ -12,13 +12,10 @@ 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 3defe8e..6d5f1a8 100644 --- a/sop-java-picocli/src/main/resources/msg_list-profiles.properties +++ b/sop-java-picocli/src/main/resources/msg_list-profiles.properties @@ -4,13 +4,10 @@ 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 093aeb3..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 @@ -4,12 +4,10 @@ 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 deleted file mode 100644 index 8c0bfa3..0000000 --- a/sop-java-picocli/src/main/resources/msg_merge-certs.properties +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Paul Schaub -# -# SPDX-License-Identifier: Apache-2.0 -usage.headerHeading=Merge OpenPGP certificates%n -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 - -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.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 deleted file mode 100644 index b1f008c..0000000 --- a/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties +++ /dev/null @@ -1,19 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Paul Schaub -# -# SPDX-License-Identifier: Apache-2.0 -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 - -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=%nHinweis:%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_revoke-key.properties b/sop-java-picocli/src/main/resources/msg_revoke-key.properties index f68b774..c7d72b3 100644 --- a/sop-java-picocli/src/main/resources/msg_revoke-key.properties +++ b/sop-java-picocli/src/main/resources/msg_revoke-key.properties @@ -7,15 +7,10 @@ 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=D%nescription:%n +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_revoke-key_de.properties b/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties index fa8c5b4..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 @@ -7,13 +7,10 @@ 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 520533a..52c5368 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -2,21 +2,15 @@ # # 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 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading=%nCommands:%n -usage.optionListHeading=%nOptions:%n -usage.parameterListHeading=%nParameters:%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 @@ -40,10 +34,6 @@ 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 -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 @@ -80,8 +70,6 @@ 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_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 99d28a7..5900f39 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -2,21 +2,15 @@ # # 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 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading=%nOptionen:%n -usage.parameterListHeading=%nParameter:%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 @@ -40,10 +34,6 @@ 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 -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-picocli/src/main/resources/msg_update-key.properties b/sop-java-picocli/src/main/resources/msg_update-key.properties deleted file mode 100644 index 0b5243e..0000000 --- a/sop-java-picocli/src/main/resources/msg_update-key.properties +++ /dev/null @@ -1,24 +0,0 @@ -# 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-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 -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.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 deleted file mode 100644 index 91e5532..0000000 --- a/sop-java-picocli/src/main/resources/msg_update-key_de.properties +++ /dev/null @@ -1,21 +0,0 @@ -# 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-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 -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.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_validate-userid.properties b/sop-java-picocli/src/main/resources/msg_validate-userid.properties deleted file mode 100644 index d25fa3a..0000000 --- a/sop-java-picocli/src/main/resources/msg_validate-userid.properties +++ /dev/null @@ -1,20 +0,0 @@ -# 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 - -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 -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 deleted file mode 100644 index f919465..0000000 --- a/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties +++ /dev/null @@ -1,20 +0,0 @@ -# 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 - -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 -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_version.properties b/sop-java-picocli/src/main/resources/msg_version.properties index 1327a78..9e1451b 100644 --- a/sop-java-picocli/src/main/resources/msg_version.properties +++ b/sop-java-picocli/src/main/resources/msg_version.properties @@ -5,13 +5,10 @@ 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 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 c99045c..608b0c6 100644 --- a/sop-java-picocli/src/main/resources/msg_version_de.properties +++ b/sop-java-picocli/src/main/resources/msg_version_de.properties @@ -5,13 +5,10 @@ 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 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/test/java/sop/cli/picocli/SOPTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java index 62c7581..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 @@ -6,18 +6,16 @@ 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; import sop.operation.Armor; -import sop.operation.CertifyUserId; import sop.operation.ChangeKeyPassword; import sop.operation.Dearmor; import sop.operation.Decrypt; @@ -30,52 +28,31 @@ 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 { @Test + @ExpectSystemExitWithStatus(SOPGPException.UnsupportedSubcommand.EXIT_CODE) public void assertExitOnInvalidSubcommand() { SOP sop = mock(SOP.class); SopCLI.setSopInstance(sop); - assertUnsupportedSubcommand(() -> SopCLI.execute("invalid")); + SopCLI.main(new String[] {"invalid"}); } @Test + @ExpectSystemExitWithStatus(1) public void assertThrowsIfNoSOPBackendSet() { - SopCLI.setSopInstance(null); - // At this point, no SOP backend is set, so an InvalidStateException triggers error code 1 - assertGenericError(() -> SopCLI.execute("armor")); + SopCLI.SOP_INSTANCE = null; + // At this point, no SOP backend is set, so an InvalidStateException triggers exit(1) + SopCLI.main(new String[] {"armor"}); } @Test public void UnsupportedSubcommandsTest() { SOP nullCommandSOP = new SOP() { - @Override - public ValidateUserId validateUserId() { - return null; - } - - @Override - public CertifyUserId certifyUserId() { - return null; - } - - @Override - public MergeCerts mergeCerts() { - return null; - } - - @Override - public UpdateKey updateKey() { - return null; - } - @Override public Version version() { return null; @@ -164,11 +141,6 @@ 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); 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 396bc7f..aed420b 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(NullPointerException.class, () -> abstractCmd.getInput(null)); + assertThrows(IllegalArgumentException.class, () -> abstractCmd.getInput(null)); } @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 3dd4c7c..6bdbe7f 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,27 +4,28 @@ 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; 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; -import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; -import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; - public class ArmorCmdTest { private Armor armor; @@ -40,30 +41,60 @@ 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 { - assertSuccess(() -> SopCLI.execute("armor")); + SopCLI.main(new String[] {"armor"}); verify(armor, times(1)).data((InputStream) any()); } @Test - public void ifBadDataExit41() throws SOPGPException.BadData, IOException { - when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - - assertBadData(() -> SopCLI.execute("armor")); + @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 { + when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); + + SopCLI.main(new String[] {"armor"}); + } + + @Test + @FailOnSystemExit public void ifNoErrorsNoExit() { when(sop.armor()).thenReturn(armor); - assertSuccess(() -> SopCLI.execute("armor")); + SopCLI.main(new String[] {"armor"}); } private static Ready nopReady() { return new Ready() { @Override - public void writeTo(@Nonnull OutputStream outputStream) { + public void writeTo(OutputStream outputStream) { } }; } 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 b0a9fd8..875eaed 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,13 +9,12 @@ 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; @@ -49,13 +48,14 @@ public class DearmorCmdTest { @Test public void assertDataIsCalled() throws IOException, SOPGPException.BadData { - assertSuccess(() -> SopCLI.execute("dearmor")); + SopCLI.main(new String[] {"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"))); - assertBadData(() -> SopCLI.execute("dearmor")); + SopCLI.main(new String[] {"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 b7cb8bc..edfd052 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,6 +4,7 @@ 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; @@ -41,18 +42,6 @@ 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 { @@ -85,47 +74,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.")); - assertMissingArg(() -> SopCLI.execute("decrypt")); + SopCLI.main(new String[] {"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())); - assertBadData(() -> SopCLI.execute("decrypt")); + SopCLI.main(new String[] {"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()); - assertPasswordNotHumanReadable(() -> - SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath()) - ); + SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); } @Test public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("orange"); - assertSuccess(() -> SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath())); + SopCLI.main(new String[] {"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.")); - assertUnsupportedOption(() -> - SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath()) - ); + SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); } @Test public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption { Date now = new Date(); - assertSuccess(() -> SopCLI.execute("decrypt")); + SopCLI.main(new String[] {"decrypt"}); verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME); verify(decrypt, times(1)).verifyNotAfter( ArgumentMatchers.argThat(argument -> { @@ -136,8 +125,7 @@ public class DecryptCmdTest { @Test public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption { - assertSuccess(() -> - SopCLI.execute("decrypt", "--verify-not-before", "-", "--verify-not-after", "-")); + SopCLI.main(new String[] {"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); } @@ -150,57 +138,54 @@ public class DecryptCmdTest { return Math.abs(now.getTime() - argument.getTime()) <= 1000; }; - assertSuccess(() -> - SopCLI.execute("decrypt", "--verify-not-before", "now", "--verify-not-after", "now")); + SopCLI.main(new String[] {"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) - assertGenericError(() -> - SopCLI.execute("decrypt", "--verify-not-before", "invalid")); + SopCLI.main(new String[] {"decrypt", "--verify-not-before", "invalid"}); } @Test + @ExpectSystemExitWithStatus(1) public void assertMalformedDateInNotAfterCausesExit1() { // ParserException causes exit(1) - assertGenericError(() -> - SopCLI.execute("decrypt", "--verify-not-after", "invalid")); + SopCLI.main(new String[] {"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.")); - assertUnsupportedOption(() -> - SopCLI.execute("decrypt", "--verify-not-after", "now")); + when(decrypt.verifyNotAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); + SopCLI.main(new String[] {"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.")); - assertUnsupportedOption(() -> - SopCLI.execute("decrypt", "--verify-not-before", "now")); + when(decrypt.verifyNotBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); + SopCLI.main(new String[] {"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(); - assertOutputExists(() -> - SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath())); + SopCLI.main(new String[] {"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(); - assertUnsupportedOption(() -> - SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath())); + SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); } @Test @@ -225,10 +210,8 @@ public class DecryptCmdTest { File verificationsFile = new File(tempDir.toFile(), "verifications"); File keyFile = new File(tempDir.toFile(), "key.asc"); keyFile.createNewFile(); - assertSuccess(() -> - SopCLI.execute("decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(), - "--verifications-out", verificationsFile.getAbsolutePath(), "--verify-with", - keyFile.getAbsolutePath())); + SopCLI.main(new String[] {"decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(), + "--verifications-out", verificationsFile.getAbsolutePath(), "--verify-with", keyFile.getAbsolutePath()}); ByteArrayOutputStream bytesInFile = new ByteArrayOutputStream(); try (FileInputStream fileIn = new FileInputStream(sessionKeyFile)) { @@ -258,10 +241,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()); - assertCannotDecrypt(() -> - SopCLI.execute("decrypt")); + SopCLI.main(new String[] {"decrypt"}); } @Test @@ -275,32 +258,30 @@ public class DecryptCmdTest { return new DecryptionResult(null, Collections.emptyList()); } }); - assertSuccess(() -> - SopCLI.execute("decrypt", "--verify-with", tempFile.getAbsolutePath(), "--verifications-out", - verifyOut.getAbsolutePath())); + SopCLI.main(new String[] {"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"); - assertBadData(() -> - SopCLI.execute("decrypt", "--verify-with", tempFile.getAbsolutePath())); + SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath()}); } @Test + @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void unexistentCertFileCausesExit61() { - assertMissingInput(() -> - SopCLI.execute("decrypt", "--verify-with", "invalid")); + SopCLI.main(new String[] {"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"); - assertOutputExists(() -> SopCLI.execute("decrypt", "--verifications-out", - existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath())); + SopCLI.main(new String[] {"decrypt", "--verify-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); } @Test @@ -324,9 +305,7 @@ public class DecryptCmdTest { } }); - assertSuccess(() -> - SopCLI.execute("decrypt", "--verifications-out", verifyOut.getAbsolutePath(), - "--verify-with", certFile.getAbsolutePath())); + SopCLI.main(new String[] {"decrypt", "--verify-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); @@ -341,64 +320,66 @@ public class DecryptCmdTest { File sessionKeyFile1 = TestFileUtil.writeTempStringFile(key1.toString()); File sessionKeyFile2 = TestFileUtil.writeTempStringFile(key2.toString()); - assertSuccess(() -> - SopCLI.execute("decrypt", - "--with-session-key", sessionKeyFile1.getAbsolutePath(), - "--with-session-key", sessionKeyFile2.getAbsolutePath())); + SopCLI.main(new String[] {"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"); - assertGenericError(() -> - SopCLI.execute("decrypt", - "--with-session-key", sessionKeyFile.getAbsolutePath())); + SopCLI.main(new String[] {"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"); - assertBadData(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath())); + SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); } @Test + @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void assertKeyFileNotFoundCausesExit61() { - assertMissingInput(() -> SopCLI.execute("decrypt", "nonexistent-key")); + SopCLI.main(new String[] {"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"); - assertKeyIsProtected(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath())); + SopCLI.main(new String[] {"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"); - assertUnsupportedAsymmetricAlgo(() -> - SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath())); + SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); } @Test + @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void assertMissingPassphraseFileCausesExit61() { - assertMissingInput(() -> - SopCLI.execute("decrypt", "--with-password", "missing")); + SopCLI.main(new String[] {"decrypt", "--with-password", "missing"}); } @Test + @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void assertMissingSessionKeyFileCausesExit61() { - assertMissingInput(() -> - SopCLI.execute("decrypt", "--with-session-key", "missing")); + SopCLI.main(new String[] {"decrypt", "--with-session-key", "missing"}); } @Test + @ExpectSystemExitWithStatus(SOPGPException.IncompleteVerification.EXIT_CODE) public void verifyOutWithoutVerifyWithCausesExit23() { - assertIncompleteVerification(() -> - SopCLI.execute("decrypt", "--verifications-out", "out.file")); + SopCLI.main(new String[] {"decrypt", "--verify-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 85ae052..73ec9cb 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,40 +4,28 @@ package sop.cli.picocli.commands; -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; 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; + +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 { @@ -46,10 +34,10 @@ public class EncryptCmdTest { @BeforeEach public void mockComponents() throws IOException { encrypt = mock(Encrypt.class); - when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult() { + when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() { @Override - public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException { - return new EncryptionResult(null); + public void writeTo(OutputStream outputStream) { + } }); @@ -60,50 +48,48 @@ public class EncryptCmdTest { } @Test - public void missingBothPasswordAndCertFileCausesMissingArg() { - assertMissingArg(() -> - SopCLI.execute("encrypt", "--no-armor")); + @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) + public void missingBothPasswordAndCertFileCauseExit19() { + SopCLI.main(new String[] {"encrypt", "--no-armor"}); } @Test - public void as_unsupportedEncryptAsCausesUnsupportedOption() throws SOPGPException.UnsupportedOption { + @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) + public void as_unsupportedEncryptAsCausesExit37() throws SOPGPException.UnsupportedOption { when(encrypt.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting encryption mode not supported.")); - assertUnsupportedOption(() -> - SopCLI.execute("encrypt", "--as", "Binary")); + SopCLI.main(new String[] {"encrypt", "--as", "Binary"}); } @Test - public void as_invalidModeOptionCausesUnsupportedOption() { - assertUnsupportedOption(() -> - SopCLI.execute("encrypt", "--as", "invalid")); + @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) + public void as_invalidModeOptionCausesExit37() { + SopCLI.main(new String[] {"encrypt", "--as", "invalid"}); } @Test public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("0rbit"); for (EncryptAs mode : EncryptAs.values()) { - assertSuccess(() -> - SopCLI.execute("encrypt", "--as", mode.name(), - "--with-password", passwordFile.getAbsolutePath())); + SopCLI.main(new String[] {"encrypt", "--as", mode.name(), "--with-password", passwordFile.getAbsolutePath()}); verify(encrypt, times(1)).mode(mode); } } @Test - public void withPassword_notHumanReadablePasswordCausesPWNotHumanReadable() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { + @ExpectSystemExitWithStatus(SOPGPException.PasswordNotHumanReadable.EXIT_CODE) + public void withPassword_notHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable()); File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable"); - assertPasswordNotHumanReadable(() -> - SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); } @Test - public void withPassword_unsupportedWithPasswordCausesUnsupportedOption() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { + @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) + public void withPassword_unsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported.")); File passwordFile = TestFileUtil.writeTempStringFile("orange"); - assertUnsupportedOption(() -> - SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); } @Test @@ -111,115 +97,106 @@ public class EncryptCmdTest { File keyFile1 = File.createTempFile("sign-with-1-", ".asc"); File keyFile2 = File.createTempFile("sign-with-2-", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("password"); - assertSuccess(() -> - SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), - "--sign-with", keyFile1.getAbsolutePath(), - "--sign-with", keyFile2.getAbsolutePath())); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile1.getAbsolutePath(), "--sign-with", keyFile2.getAbsolutePath()}); verify(encrypt, times(2)).signWith((InputStream) any()); } @Test - public void signWith_nonExistentKeyFileCausesMissingInput() { - assertMissingInput(() -> - SopCLI.execute("encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc")); + @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) + public void signWith_nonExistentKeyFileCausesExit61() { + SopCLI.main(new String[] {"encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"}); } @Test - public void signWith_keyIsProtectedCausesKeyIsProtected() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + @ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE) + public void signWith_keyIsProtectedCausesExit67() 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"); - assertKeyIsProtected(() -> - SopCLI.execute("encrypt", "--sign-with", keyFile.getAbsolutePath(), - "--with-password", passwordFile.getAbsolutePath())); + SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", passwordFile.getAbsolutePath()}); } @Test - public void signWith_unsupportedAsymmetricAlgoCausesUnsupportedAsymAlgo() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) + public void signWith_unsupportedAsymmetricAlgoCausesExit13() 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"); - assertUnsupportedAsymmetricAlgo(() -> - SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), - "--sign-with", keyFile.getAbsolutePath())); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); } @Test - public void signWith_certCannotSignCausesKeyCannotSign() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { + @ExpectSystemExitWithStatus(SOPGPException.KeyCannotSign.EXIT_CODE) + public void signWith_certCannotSignCausesExit79() 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"); - assertKeyCannotSign(() -> - SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), - "--sign-with", keyFile.getAbsolutePath())); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); } @Test - public void signWith_badDataCausesBadData() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) + public void signWith_badDataCausesExit41() 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"); - assertBadData(() -> - SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), - "--sign-with", keyFile.getAbsolutePath())); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); } @Test - public void cert_nonExistentCertFileCausesMissingInput() { - assertMissingInput(() -> - SopCLI.execute("encrypt", "invalid.asc")); + @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) + public void cert_nonExistentCertFileCausesExit61() { + SopCLI.main(new String[] {"encrypt", "invalid.asc"}); } @Test - public void cert_unsupportedAsymmetricAlgorithmCausesUnsupportedAsymAlg() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { + @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) + public void cert_unsupportedAsymmetricAlgorithmCausesExit13() 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"); - assertUnsupportedAsymmetricAlgo(() -> - SopCLI.execute("encrypt", certFile.getAbsolutePath())); + SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); } @Test - public void cert_certCannotEncryptCausesCertCannotEncrypt() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { + @ExpectSystemExitWithStatus(SOPGPException.CertCannotEncrypt.EXIT_CODE) + public void cert_certCannotEncryptCausesExit17() 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"); - assertCertCannotEncrypt(() -> - SopCLI.execute("encrypt", certFile.getAbsolutePath())); + SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); } @Test - public void cert_badDataCausesBadData() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { + @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) + public void cert_badDataCausesExit41() 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"); - assertBadData(() -> - SopCLI.execute("encrypt", certFile.getAbsolutePath())); + SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); } @Test public void noArmor_notCalledByDefault() throws IOException { File passwordFile = TestFileUtil.writeTempStringFile("clownfish"); - assertSuccess(() -> - SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); verify(encrypt, never()).noArmor(); } @Test public void noArmor_callGetsPassedDown() throws IOException { File passwordFile = TestFileUtil.writeTempStringFile("monkey"); - assertSuccess(() -> - SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), "--no-armor")); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--no-armor"}); verify(encrypt, times(1)).noArmor(); } @Test - public void writeTo_ioExceptionCausesGenericError() throws IOException { - when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult() { + @ExpectSystemExitWithStatus(1) + public void writeTo_ioExceptionCausesExit1() throws IOException { + when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() { @Override - public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException { + public void writeTo(OutputStream outputStream) throws IOException { throw new IOException(); } }); File passwordFile = TestFileUtil.writeTempStringFile("wildcat"); - assertGenericError(() -> - SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); + SopCLI.main(new String[] {"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 3b046a0..12f837d 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,14 +10,12 @@ 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; @@ -47,34 +45,32 @@ public class ExtractCertCmdTest { @Test public void noArmor_notCalledByDefault() { - assertSuccess(() -> - SopCLI.execute("extract-cert")); + SopCLI.main(new String[] {"extract-cert"}); verify(extractCert, never()).noArmor(); } @Test public void noArmor_passedDown() { - assertSuccess(() -> - SopCLI.execute("extract-cert", "--no-armor")); + SopCLI.main(new String[] {"extract-cert", "--no-armor"}); verify(extractCert, times(1)).noArmor(); } @Test - public void key_ioExceptionCausesGenericError() throws IOException, SOPGPException.BadData { + @ExpectSystemExitWithStatus(1) + public void key_ioExceptionCausesExit1() throws IOException, SOPGPException.BadData { when(extractCert.key((InputStream) any())).thenReturn(new Ready() { @Override public void writeTo(OutputStream outputStream) throws IOException { throw new IOException(); } }); - assertGenericError(() -> - SopCLI.execute("extract-cert")); + SopCLI.main(new String[] {"extract-cert"}); } @Test - public void key_badDataCausesBadData() throws IOException, SOPGPException.BadData { + @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) + public void key_badDataCausesExit41() throws IOException, SOPGPException.BadData { when(extractCert.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - assertBadData(() -> - SopCLI.execute("extract-cert")); + SopCLI.main(new String[] {"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 126c851..e7ebf1a 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,14 +10,11 @@ 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; @@ -50,22 +47,19 @@ public class GenerateKeyCmdTest { @Test public void noArmor_notCalledByDefault() { - assertSuccess(() -> - SopCLI.execute("generate-key", "Alice")); + SopCLI.main(new String[] {"generate-key", "Alice"}); verify(generateKey, never()).noArmor(); } @Test public void noArmor_passedDown() { - assertSuccess(() -> - SopCLI.execute("generate-key", "--no-armor", "Alice")); + SopCLI.main(new String[] {"generate-key", "--no-armor", "Alice"}); verify(generateKey, times(1)).noArmor(); } @Test public void userId_multipleUserIdsPassedDownInProperOrder() { - assertSuccess(() -> - SopCLI.execute("generate-key", "Alice ", "Bob ")); + SopCLI.main(new String[] {"generate-key", "Alice ", "Bob "}); InOrder inOrder = Mockito.inOrder(generateKey); inOrder.verify(generateKey).userId("Alice "); @@ -75,32 +69,30 @@ 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.")); - assertMissingArg(() -> - SopCLI.execute("generate-key")); + SopCLI.main(new String[] {"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())); - assertUnsupportedAsymmetricAlgo(() -> - SopCLI.execute("generate-key", "Alice")); - + SopCLI.main(new String[] {"generate-key", "Alice"}); } @Test - public void ioExceptionCausesGenericError() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { + @ExpectSystemExitWithStatus(1) + public void ioExceptionCausesExit1() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { when(generateKey.generate()).thenReturn(new Ready() { @Override public void writeTo(OutputStream outputStream) throws IOException { throw new IOException(); } }); - - assertGenericError(() -> - SopCLI.execute("generate-key", "Alice")); + SopCLI.main(new String[] {"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 a230aaa..3a16c61 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,6 +4,7 @@ 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; @@ -25,8 +26,6 @@ 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 { @@ -42,9 +41,9 @@ public class InlineDetachCmdTest { } @Test - public void testMissingSignaturesOutResultsInMissingArg() { - assertMissingArg(() -> - SopCLI.execute("inline-detach")); + @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) + public void testMissingSignaturesOutResultsInExit19() { + SopCLI.main(new String[] {"inline-detach"}); } @Test @@ -68,8 +67,7 @@ public class InlineDetachCmdTest { } }); - assertSuccess(() -> - SopCLI.execute("inline-detach", "--signatures-out", tempFile.getAbsolutePath(), "--no-armor")); + SopCLI.main(new String[] {"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 324d39a..c3d6b59 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,20 +10,13 @@ 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; @@ -61,77 +54,70 @@ public class SignCmdTest { @Test public void as_optionsAreCaseInsensitive() { - assertSuccess(() -> - SopCLI.execute("sign", "--as", "Binary", keyFile.getAbsolutePath())); - assertSuccess(() -> - SopCLI.execute("sign", "--as", "binary", keyFile.getAbsolutePath())); - assertSuccess(() -> - SopCLI.execute("sign", "--as", "BINARY", keyFile.getAbsolutePath())); + 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()}); } @Test + @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void as_invalidOptionCausesExit37() { - assertUnsupportedOption(() -> - SopCLI.execute("sign", "--as", "Invalid", keyFile.getAbsolutePath())); + SopCLI.main(new String[] {"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.")); - assertUnsupportedOption(() -> - SopCLI.execute("sign", "--as", "binary", keyFile.getAbsolutePath())); + SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); } @Test + @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void key_nonExistentKeyFileCausesExit61() { - assertMissingInput(() -> - SopCLI.execute("sign", "invalid.asc")); + SopCLI.main(new String[] {"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()); - assertKeyIsProtected(() -> - SopCLI.execute("sign", keyFile.getAbsolutePath())); + SopCLI.main(new String[] {"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())); - assertBadData(() -> - SopCLI.execute("sign", keyFile.getAbsolutePath())); + SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); } @Test + @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void key_missingKeyFileCausesExit19() { - assertMissingArg(() -> - SopCLI.execute("sign")); + SopCLI.main(new String[] {"sign"}); } @Test public void noArmor_notCalledByDefault() { - assertSuccess(() -> - SopCLI.execute("sign", keyFile.getAbsolutePath())); + SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); verify(detachedSign, never()).noArmor(); } @Test public void noArmor_passedDown() { - assertSuccess(() -> - SopCLI.execute("sign", "--no-armor", keyFile.getAbsolutePath())); + SopCLI.main(new String[] {"sign", "--no-armor", keyFile.getAbsolutePath()}); verify(detachedSign, times(1)).noArmor(); } @Test public void withKeyPassword_passedDown() { - assertSuccess(() -> - SopCLI.execute("sign", - "--with-key-password", passFile.getAbsolutePath(), - keyFile.getAbsolutePath())); + SopCLI.main(new String[] {"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 @@ -139,14 +125,13 @@ public class SignCmdTest { throw new IOException(); } }); - assertGenericError(() -> - SopCLI.execute("sign", keyFile.getAbsolutePath())); + SopCLI.main(new String[] {"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()); - assertExpectedText(() -> - SopCLI.execute("sign", keyFile.getAbsolutePath())); + SopCLI.main(new String[] {"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 3c9724f..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 @@ -10,11 +10,6 @@ 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; @@ -26,6 +21,7 @@ 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; @@ -80,75 +76,60 @@ public class VerifyCmdTest { @Test public void notAfter_passedDown() throws SOPGPException.UnsupportedOption, ParseException { Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"); - assertSuccess(() -> - SopCLI.execute("verify", "--not-after", "2019-10-29T18:36:45Z", - signature.getAbsolutePath(), cert.getAbsolutePath())); + SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); verify(detachedVerify, times(1)).notAfter(date); } @Test public void notAfter_now() throws SOPGPException.UnsupportedOption { Date now = new Date(); - assertSuccess(() -> - SopCLI.execute("verify", "--not-after", "now", - signature.getAbsolutePath(), cert.getAbsolutePath())); + SopCLI.main(new String[] {"verify", "--not-after", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); verify(detachedVerify, times(1)).notAfter(dateMatcher(now)); } @Test public void notAfter_dashCountsAsEndOfTime() throws SOPGPException.UnsupportedOption { - assertSuccess(() -> - SopCLI.execute("verify", "--not-after", "-", - signature.getAbsolutePath(), cert.getAbsolutePath())); + SopCLI.main(new String[] {"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.")); - assertUnsupportedOption(() -> - SopCLI.execute("verify", "--not-after", "2019-10-29T18:36:45Z", - signature.getAbsolutePath(), cert.getAbsolutePath())); + SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); } @Test public void notBefore_passedDown() throws SOPGPException.UnsupportedOption, ParseException { Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"); - assertSuccess(() -> - SopCLI.execute("verify", "--not-before", "2019-10-29T18:36:45Z", - signature.getAbsolutePath(), cert.getAbsolutePath())); + SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); verify(detachedVerify, times(1)).notBefore(date); } @Test public void notBefore_now() throws SOPGPException.UnsupportedOption { Date now = new Date(); - assertSuccess(() -> - SopCLI.execute("verify", "--not-before", "now", - signature.getAbsolutePath(), cert.getAbsolutePath())); + SopCLI.main(new String[] {"verify", "--not-before", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); verify(detachedVerify, times(1)).notBefore(dateMatcher(now)); } @Test public void notBefore_dashCountsAsBeginningOfTime() throws SOPGPException.UnsupportedOption { - assertSuccess(() -> - SopCLI.execute("verify", "--not-before", "-", - signature.getAbsolutePath(), cert.getAbsolutePath())); + SopCLI.main(new String[] {"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.")); - assertUnsupportedOption(() -> - SopCLI.execute("verify", "--not-before", "2019-10-29T18:36:45Z", - signature.getAbsolutePath(), cert.getAbsolutePath())); + SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); } @Test public void notBeforeAndNotAfterAreCalledWithDefaultValues() throws SOPGPException.UnsupportedOption { - assertSuccess(() -> - SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); + SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); verify(detachedVerify, times(1)).notAfter(dateMatcher(new Date())); verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME); } @@ -158,43 +139,43 @@ public class VerifyCmdTest { } @Test + @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void cert_fileNotFoundCausesExit61() { - assertMissingInput(() -> - SopCLI.execute("verify", signature.getAbsolutePath(), "invalid.asc")); + SopCLI.main(new String[] {"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())); - assertBadData(() -> - SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); + SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); } @Test + @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void signature_fileNotFoundCausesExit61() { - assertMissingInput(() -> - SopCLI.execute("verify", "invalid.sig", cert.getAbsolutePath())); + SopCLI.main(new String[] {"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())); - assertBadData(() -> - SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); + SopCLI.main(new String[] {"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()); - assertNoSignature(() -> - SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); + SopCLI.main(new String[] {"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())); - assertBadData(() -> - SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); + SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); } @Test @@ -211,8 +192,7 @@ public class VerifyCmdTest { ByteArrayOutputStream out = new ByteArrayOutputStream(); System.setOut(new PrintStream(out)); - assertSuccess(() -> - SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); + SopCLI.main(new String[] {"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 92850bd..e284e35 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,18 +4,18 @@ package sop.cli.picocli.commands; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import sop.SOP; -import sop.cli.picocli.SopCLI; -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; + +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; public class VersionCmdTest { @@ -29,8 +29,6 @@ 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); @@ -38,41 +36,26 @@ public class VersionCmdTest { @Test public void assertVersionCommandWorks() { - assertSuccess(() -> - SopCLI.execute("version")); + SopCLI.main(new String[] {"version"}); verify(version, times(1)).getVersion(); verify(version, times(1)).getName(); } @Test public void assertExtendedVersionCommandWorks() { - assertSuccess(() -> - SopCLI.execute("version", "--extended")); + SopCLI.main(new String[] {"version", "--extended"}); verify(version, times(1)).getExtendedVersion(); } @Test public void assertBackendVersionCommandWorks() { - assertSuccess(() -> - SopCLI.execute("version", "--backend")); + SopCLI.main(new String[] {"version", "--backend"}); verify(version, times(1)).getBackendVersion(); } @Test - public void assertSpecVersionCommandWorks() { - assertSuccess(() -> - SopCLI.execute("version", "--sop-spec")); - } - - @Test - public void assertSOPVVersionCommandWorks() { - assertSuccess(() -> - SopCLI.execute("version", "--sopv")); - } - - @Test + @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertInvalidOptionResultsInExit37() { - assertUnsupportedOption(() -> - SopCLI.execute("version", "--invalid")); + SopCLI.main(new String[] {"version", "--invalid"}); } } diff --git a/sop-java-testfixtures/build.gradle b/sop-java-testfixtures/build.gradle deleted file mode 100644 index d3d4a1e..0000000 --- a/sop-java-testfixtures/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -// 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-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOption.java b/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOption.java deleted file mode 100644 index cbd0746..0000000 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOption.java +++ /dev/null @@ -1,18 +0,0 @@ -// 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 deleted file mode 100644 index 0bf366d..0000000 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOptionExtension.java +++ /dev/null @@ -1,26 +0,0 @@ -// 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/java/sop/testsuite/assertions/SopExecutionAssertions.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/SopExecutionAssertions.java deleted file mode 100644 index bd07f0b..0000000 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/SopExecutionAssertions.java +++ /dev/null @@ -1,235 +0,0 @@ -// 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()); - } -} 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 deleted file mode 100644 index 855c23d..0000000 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java +++ /dev/null @@ -1,193 +0,0 @@ -// 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.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class CertifyValidateUserIdTest extends AbstractSOPTest { - - static Stream provideInstances() { - return AbstractSOPTest.provideBackends(); - } - - @ParameterizedTest - @MethodSource("provideInstances") - public void certifyUserId(SOP sop) throws IOException { - byte[] aliceKey = assumeSupported(sop::generateKey) - .withKeyPassword("sw0rdf1sh") - .userId("Alice ") - .generate() - .getBytes(); - byte[] aliceCert = assumeSupported(sop::extractCert) - .key(aliceKey) - .getBytes(); - - byte[] bobKey = assumeSupported(sop::generateKey) - .userId("Bob ") - .generate() - .getBytes(); - byte[] bobCert = assumeSupported(sop::extractCert) - .key(bobKey) - .getBytes(); - - // Alice has her own user-id self-certified - assertTrue(assumeSupported(sop::validateUserId) - .authorities(aliceCert) - .userId("Alice ") - .subjects(aliceCert), - "Alice accepts her own self-certified user-id"); - - // Alice has not yet certified Bobs user-id - assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> - assumeSupported(sop::validateUserId) - .authorities(aliceCert) - .userId("Bob ") - .subjects(bobCert), - "Alice has not yet certified Bobs user-id"); - - byte[] bobCertifiedByAlice = assumeSupported(sop::certifyUserId) - .userId("Bob ") - .withKeyPassword("sw0rdf1sh") - .keys(aliceKey) - .certs(bobCert) - .getBytes(); - - assertTrue(assumeSupported(sop::validateUserId) - .userId("Bob ") - .authorities(aliceCert) - .subjects(bobCertifiedByAlice), - "Alice accepts Bobs user-id after she certified it"); - } - - @ParameterizedTest - @MethodSource("provideInstances") - public void certifyUserIdUnarmored(SOP sop) throws IOException { - byte[] aliceKey = assumeSupported(sop::generateKey) - .noArmor() - .withKeyPassword("sw0rdf1sh") - .userId("Alice ") - .generate() - .getBytes(); - byte[] aliceCert = assumeSupported(sop::extractCert) - .noArmor() - .key(aliceKey) - .getBytes(); - - byte[] bobKey = assumeSupported(sop::generateKey) - .noArmor() - .userId("Bob ") - .generate() - .getBytes(); - byte[] bobCert = assumeSupported(sop::extractCert) - .noArmor() - .key(bobKey) - .getBytes(); - - byte[] bobCertifiedByAlice = assumeSupported(sop::certifyUserId) - .noArmor() - .userId("Bob ") - .withKeyPassword("sw0rdf1sh") - .keys(aliceKey) - .certs(bobCert) - .getBytes(); - - assertTrue(assumeSupported(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 { - byte[] aliceKey = assumeSupported(sop::generateKey) - .userId("Alice ") - .generate() - .getBytes(); - byte[] aliceCert = assumeSupported(sop::extractCert) - .key(aliceKey) - .getBytes(); - - byte[] bobKey = assumeSupported(sop::generateKey) - .userId("Bob ") - .generate() - .getBytes(); - byte[] bobCert = assumeSupported(sop::extractCert) - .key(bobKey) - .getBytes(); - - assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> - 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 = assumeSupported(sop::certifyUserId) - .userId("Bobby") - .noRequireSelfSig() - .keys(aliceKey) - .certs(bobCert) - .getBytes(); - - assertTrue(assumeSupported(sop::validateUserId) - .userId("Bobby") - .authorities(aliceCert) - .subjects(bobWithPetName), - "Alice accepts the pet-name she gave to Bob"); - - assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> - assumeSupported(sop::validateUserId) - .userId("Bobby") - .authorities(bobWithPetName) - .subjects(bobWithPetName), - "Bob does not accept the pet-name Alice gave him"); - } - - @ParameterizedTest - @MethodSource("provideInstances") - public void certifyWithRevokedKey(SOP sop) throws IOException { - byte[] aliceKey = assumeSupported(sop::generateKey) - .userId("Alice ") - .generate() - .getBytes(); - byte[] aliceRevokedCert = assumeSupported(sop::revokeKey) - .keys(aliceKey) - .getBytes(); - byte[] aliceRevokedKey = assumeSupported(sop::updateKey) - .mergeCerts(aliceRevokedCert) - .key(aliceKey) - .getBytes(); - - byte[] bobKey = assumeSupported(sop::generateKey) - .userId("Bob ") - .generate() - .getBytes(); - byte[] bobCert = assumeSupported(sop::extractCert) - .key(bobKey) - .getBytes(); - - assertThrows(SOPGPException.KeyCannotCertify.class, () -> - assumeSupported(sop::certifyUserId) - .userId("Bob ") - .keys(aliceRevokedKey) - .certs(bobCert) - .getBytes()); - } -} 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 deleted file mode 100644 index 501f53c..0000000 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java +++ /dev/null @@ -1,164 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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; -import sop.SOP; - -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 = assumeSupported(sop::generateKey) - .userId("Alice ") - .generate() - .getBytes(); - - byte[] cert = assumeSupported(sop::extractCert) - .key(key) - .getBytes(); - - byte[] merged = assumeSupported(sop::mergeCerts) - .updates(cert) - .baseCertificates(cert) - .getBytes(); - - assertArrayEquals(cert, merged); - } - - @ParameterizedTest - @MethodSource("provideInstances") - public void testMergeWithItselfArmored(SOP sop) throws IOException { - byte[] key = assumeSupported(sop::generateKey) - .noArmor() - .userId("Alice ") - .generate() - .getBytes(); - - byte[] cert = assumeSupported(sop::extractCert) - .key(key) - .getBytes(); - - byte[] merged = assumeSupported(sop::mergeCerts) - .updates(cert) - .baseCertificates(cert) - .getBytes(); - - assertArrayEquals(cert, merged); - } - - @ParameterizedTest - @MethodSource("provideInstances") - public void testMergeWithItselfViaBase(SOP sop) throws IOException { - byte[] key = assumeSupported(sop::generateKey) - .userId("Alice ") - .generate() - .getBytes(); - - byte[] cert = assumeSupported(sop::extractCert) - .key(key) - .getBytes(); - - byte[] certs = ArraysKt.plus(cert, cert); - - byte[] merged = assumeSupported(sop::mergeCerts) - .updates(cert) - .baseCertificates(certs) - .getBytes(); - - assertArrayEquals(cert, merged); - } - - @ParameterizedTest - @MethodSource("provideInstances") - public void testApplyBaseToUpdate(SOP sop) throws IOException { - byte[] key = assumeSupported(sop::generateKey) - .userId("Alice ") - .generate() - .getBytes(); - - byte[] cert = assumeSupported(sop::extractCert) - .key(key) - .getBytes(); - - byte[] update = assumeSupported(sop::revokeKey) - .keys(key) - .getBytes(); - - byte[] merged = assumeSupported(sop::mergeCerts) - .updates(cert) - .baseCertificates(update) - .getBytes(); - - assertArrayEquals(update, merged); - } - - @ParameterizedTest - @MethodSource("provideInstances") - public void testApplyUpdateToBase(SOP sop) throws IOException { - byte[] key = assumeSupported(sop::generateKey) - .userId("Alice ") - .generate() - .getBytes(); - - byte[] cert = assumeSupported(sop::extractCert) - .key(key) - .getBytes(); - - byte[] update = assumeSupported(sop::revokeKey) - .keys(key) - .getBytes(); - - byte[] merged = assumeSupported(sop::mergeCerts) - .updates(update) - .baseCertificates(cert) - .getBytes(); - - assertArrayEquals(update, merged); - } - - @ParameterizedTest - @MethodSource("provideInstances") - public void testApplyUpdateToMissingBaseDoesNothing(SOP sop) throws IOException { - byte[] aliceKey = assumeSupported(sop::generateKey) - .userId("Alice ") - .generate() - .getBytes(); - - byte[] aliceCert = assumeSupported(sop::extractCert) - .key(aliceKey) - .getBytes(); - - byte[] bobKey = assumeSupported(sop::generateKey) - .userId("Bob ") - .generate() - .getBytes(); - - byte[] bobCert = assumeSupported(sop::extractCert) - .key(bobKey) - .getBytes(); - - byte[] merged = assumeSupported(sop::mergeCerts) - .updates(bobCert) - .baseCertificates(aliceCert) - .getBytes(); - - assertArrayEquals(aliceCert, merged); - } - -} diff --git a/sop-java/build.gradle b/sop-java/build.gradle index c6f4e4e..ca546bf 100644 --- a/sop-java/build.gradle +++ b/sop-java/build.gradle @@ -1,12 +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' + id 'java-test-fixtures' } group 'org.pgpainless' @@ -19,19 +17,14 @@ 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(project(":sop-java-testfixtures")) + 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" } -processResources { - filter ReplaceTokens, tokens: [ - "project.version": project.version.toString() - ] -} - test { useJUnitPlatform() } diff --git a/sop-java/src/main/java/sop/ByteArrayAndResult.java b/sop-java/src/main/java/sop/ByteArrayAndResult.java new file mode 100644 index 0000000..fd2b39a --- /dev/null +++ b/sop-java/src/main/java/sop/ByteArrayAndResult.java @@ -0,0 +1,50 @@ +// 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/java/sop/DecryptionResult.java b/sop-java/src/main/java/sop/DecryptionResult.java new file mode 100644 index 0000000..4f0e1ab --- /dev/null +++ b/sop-java/src/main/java/sop/DecryptionResult.java @@ -0,0 +1,29 @@ +// 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/java/sop/MicAlg.java b/sop-java/src/main/java/sop/MicAlg.java new file mode 100644 index 0000000..5bee787 --- /dev/null +++ b/sop-java/src/main/java/sop/MicAlg.java @@ -0,0 +1,55 @@ +// 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/java/sop/Profile.java b/sop-java/src/main/java/sop/Profile.java new file mode 100644 index 0000000..4ea9e71 --- /dev/null +++ b/sop-java/src/main/java/sop/Profile.java @@ -0,0 +1,143 @@ +// 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/java/sop/Ready.java b/sop-java/src/main/java/sop/Ready.java new file mode 100644 index 0000000..71ab26e --- /dev/null +++ b/sop-java/src/main/java/sop/Ready.java @@ -0,0 +1,45 @@ +// 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/java/sop/ReadyWithResult.java b/sop-java/src/main/java/sop/ReadyWithResult.java new file mode 100644 index 0000000..9feedda --- /dev/null +++ b/sop-java/src/main/java/sop/ReadyWithResult.java @@ -0,0 +1,41 @@ +// 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/java/sop/SOP.java b/sop-java/src/main/java/sop/SOP.java new file mode 100644 index 0000000..1200e21 --- /dev/null +++ b/sop-java/src/main/java/sop/SOP.java @@ -0,0 +1,177 @@ +// 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/java/sop/SessionKey.java b/sop-java/src/main/java/sop/SessionKey.java new file mode 100644 index 0000000..722666d --- /dev/null +++ b/sop-java/src/main/java/sop/SessionKey.java @@ -0,0 +1,80 @@ +// 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/java/sop/Signatures.java b/sop-java/src/main/java/sop/Signatures.java new file mode 100644 index 0000000..dd3f000 --- /dev/null +++ b/sop-java/src/main/java/sop/Signatures.java @@ -0,0 +1,21 @@ +// 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/java/sop/SigningResult.java b/sop-java/src/main/java/sop/SigningResult.java new file mode 100644 index 0000000..1ea1ba8 --- /dev/null +++ b/sop-java/src/main/java/sop/SigningResult.java @@ -0,0 +1,50 @@ +// 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/java/sop/Verification.java b/sop-java/src/main/java/sop/Verification.java new file mode 100644 index 0000000..140e23b --- /dev/null +++ b/sop-java/src/main/java/sop/Verification.java @@ -0,0 +1,222 @@ +// 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/java/sop/enums/ArmorLabel.java b/sop-java/src/main/java/sop/enums/ArmorLabel.java new file mode 100644 index 0000000..bb97e84 --- /dev/null +++ b/sop-java/src/main/java/sop/enums/ArmorLabel.java @@ -0,0 +1,19 @@ +// 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/java/sop/enums/EncryptAs.java b/sop-java/src/main/java/sop/enums/EncryptAs.java new file mode 100644 index 0000000..7e7d4d1 --- /dev/null +++ b/sop-java/src/main/java/sop/enums/EncryptAs.java @@ -0,0 +1,16 @@ +// 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/java/sop/enums/InlineSignAs.java b/sop-java/src/main/java/sop/enums/InlineSignAs.java new file mode 100644 index 0000000..c1097df --- /dev/null +++ b/sop-java/src/main/java/sop/enums/InlineSignAs.java @@ -0,0 +1,24 @@ +// 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/java/sop/enums/SignAs.java b/sop-java/src/main/java/sop/enums/SignAs.java new file mode 100644 index 0000000..1174098 --- /dev/null +++ b/sop-java/src/main/java/sop/enums/SignAs.java @@ -0,0 +1,23 @@ +// 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/java/sop/enums/SignatureMode.java b/sop-java/src/main/java/sop/enums/SignatureMode.java new file mode 100644 index 0000000..71ce7d8 --- /dev/null +++ b/sop-java/src/main/java/sop/enums/SignatureMode.java @@ -0,0 +1,25 @@ +// 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 new file mode 100644 index 0000000..67148d3 --- /dev/null +++ b/sop-java/src/main/java/sop/enums/package-info.java @@ -0,0 +1,9 @@ +// 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/java/sop/exception/SOPGPException.java b/sop-java/src/main/java/sop/exception/SOPGPException.java new file mode 100644 index 0000000..1d13065 --- /dev/null +++ b/sop-java/src/main/java/sop/exception/SOPGPException.java @@ -0,0 +1,473 @@ +// 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 new file mode 100644 index 0000000..4abc562 --- /dev/null +++ b/sop-java/src/main/java/sop/exception/package-info.java @@ -0,0 +1,9 @@ +// 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/operation/AbstractSign.java b/sop-java/src/main/java/sop/operation/AbstractSign.java new file mode 100644 index 0000000..508d741 --- /dev/null +++ b/sop-java/src/main/java/sop/operation/AbstractSign.java @@ -0,0 +1,85 @@ +// 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/java/sop/operation/AbstractVerify.java b/sop-java/src/main/java/sop/operation/AbstractVerify.java new file mode 100644 index 0000000..51d84b7 --- /dev/null +++ b/sop-java/src/main/java/sop/operation/AbstractVerify.java @@ -0,0 +1,68 @@ +// 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/java/sop/operation/Armor.java b/sop-java/src/main/java/sop/operation/Armor.java new file mode 100644 index 0000000..a625808 --- /dev/null +++ b/sop-java/src/main/java/sop/operation/Armor.java @@ -0,0 +1,53 @@ +// 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/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/main/java/sop/operation/Dearmor.java b/sop-java/src/main/java/sop/operation/Dearmor.java new file mode 100644 index 0000000..524dc8c --- /dev/null +++ b/sop-java/src/main/java/sop/operation/Dearmor.java @@ -0,0 +1,59 @@ +// 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/java/sop/operation/Decrypt.java b/sop-java/src/main/java/sop/operation/Decrypt.java new file mode 100644 index 0000000..0123bbc --- /dev/null +++ b/sop-java/src/main/java/sop/operation/Decrypt.java @@ -0,0 +1,193 @@ +// 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/java/sop/operation/DetachedSign.java b/sop-java/src/main/java/sop/operation/DetachedSign.java new file mode 100644 index 0000000..745077d --- /dev/null +++ b/sop-java/src/main/java/sop/operation/DetachedSign.java @@ -0,0 +1,61 @@ +// 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/java/sop/operation/DetachedVerify.java b/sop-java/src/main/java/sop/operation/DetachedVerify.java new file mode 100644 index 0000000..9dee870 --- /dev/null +++ b/sop-java/src/main/java/sop/operation/DetachedVerify.java @@ -0,0 +1,45 @@ +// 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/java/sop/operation/Encrypt.java b/sop-java/src/main/java/sop/operation/Encrypt.java new file mode 100644 index 0000000..b380d32 --- /dev/null +++ b/sop-java/src/main/java/sop/operation/Encrypt.java @@ -0,0 +1,193 @@ +// 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/java/sop/operation/ExtractCert.java b/sop-java/src/main/java/sop/operation/ExtractCert.java new file mode 100644 index 0000000..a862d33 --- /dev/null +++ b/sop-java/src/main/java/sop/operation/ExtractCert.java @@ -0,0 +1,50 @@ +// 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/java/sop/operation/GenerateKey.java b/sop-java/src/main/java/sop/operation/GenerateKey.java new file mode 100644 index 0000000..77afea1 --- /dev/null +++ b/sop-java/src/main/java/sop/operation/GenerateKey.java @@ -0,0 +1,103 @@ +// 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/java/sop/operation/InlineDetach.java b/sop-java/src/main/java/sop/operation/InlineDetach.java new file mode 100644 index 0000000..aba40b1 --- /dev/null +++ b/sop-java/src/main/java/sop/operation/InlineDetach.java @@ -0,0 +1,52 @@ +// 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/java/sop/operation/InlineSign.java b/sop-java/src/main/java/sop/operation/InlineSign.java new file mode 100644 index 0000000..d45aebd --- /dev/null +++ b/sop-java/src/main/java/sop/operation/InlineSign.java @@ -0,0 +1,60 @@ +// 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/java/sop/operation/InlineVerify.java b/sop-java/src/main/java/sop/operation/InlineVerify.java new file mode 100644 index 0000000..ac662a0 --- /dev/null +++ b/sop-java/src/main/java/sop/operation/InlineVerify.java @@ -0,0 +1,54 @@ +// 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/java/sop/operation/ListProfiles.java b/sop-java/src/main/java/sop/operation/ListProfiles.java new file mode 100644 index 0000000..0c17bd6 --- /dev/null +++ b/sop-java/src/main/java/sop/operation/ListProfiles.java @@ -0,0 +1,43 @@ +// 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/java/sop/operation/RevokeKey.java b/sop-java/src/main/java/sop/operation/RevokeKey.java new file mode 100644 index 0000000..3ceb5b3 --- /dev/null +++ b/sop-java/src/main/java/sop/operation/RevokeKey.java @@ -0,0 +1,54 @@ +// 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/java/sop/operation/VerifySignatures.java b/sop-java/src/main/java/sop/operation/VerifySignatures.java new file mode 100644 index 0000000..5181514 --- /dev/null +++ b/sop-java/src/main/java/sop/operation/VerifySignatures.java @@ -0,0 +1,46 @@ +// 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/java/sop/operation/Version.java b/sop-java/src/main/java/sop/operation/Version.java new file mode 100644 index 0000000..b6d66b9 --- /dev/null +++ b/sop-java/src/main/java/sop/operation/Version.java @@ -0,0 +1,109 @@ +// 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 new file mode 100644 index 0000000..dde4d5b --- /dev/null +++ b/sop-java/src/main/java/sop/operation/package-info.java @@ -0,0 +1,9 @@ +// 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/java/sop/package-info.java b/sop-java/src/main/java/sop/package-info.java new file mode 100644 index 0000000..5ad4f52 --- /dev/null +++ b/sop-java/src/main/java/sop/package-info.java @@ -0,0 +1,8 @@ +// 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/java/sop/util/HexUtil.java b/sop-java/src/main/java/sop/util/HexUtil.java new file mode 100644 index 0000000..9b88f53 --- /dev/null +++ b/sop-java/src/main/java/sop/util/HexUtil.java @@ -0,0 +1,47 @@ +// 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/java/sop/util/Optional.java b/sop-java/src/main/java/sop/util/Optional.java new file mode 100644 index 0000000..00eb201 --- /dev/null +++ b/sop-java/src/main/java/sop/util/Optional.java @@ -0,0 +1,50 @@ +// 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/java/sop/util/ProxyOutputStream.java b/sop-java/src/main/java/sop/util/ProxyOutputStream.java new file mode 100644 index 0000000..ed24fc2 --- /dev/null +++ b/sop-java/src/main/java/sop/util/ProxyOutputStream.java @@ -0,0 +1,80 @@ +// 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/java/sop/util/UTCUtil.java b/sop-java/src/main/java/sop/util/UTCUtil.java new file mode 100644 index 0000000..96a6b9c --- /dev/null +++ b/sop-java/src/main/java/sop/util/UTCUtil.java @@ -0,0 +1,65 @@ +// 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/java/sop/util/UTF8Util.java b/sop-java/src/main/java/sop/util/UTF8Util.java new file mode 100644 index 0000000..1b4941b --- /dev/null +++ b/sop-java/src/main/java/sop/util/UTF8Util.java @@ -0,0 +1,37 @@ +// 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 new file mode 100644 index 0000000..3dd9fc1 --- /dev/null +++ b/sop-java/src/main/java/sop/util/package-info.java @@ -0,0 +1,8 @@ +// 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/ByteArrayAndResult.kt b/sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt deleted file mode 100644 index 021a2d9..0000000 --- a/sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt +++ /dev/null @@ -1,43 +0,0 @@ -// 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 - } -} diff --git a/sop-java/src/main/kotlin/sop/DecryptionResult.kt b/sop-java/src/main/kotlin/sop/DecryptionResult.kt deleted file mode 100644 index b653297..0000000 --- a/sop-java/src/main/kotlin/sop/DecryptionResult.kt +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop - -import sop.util.Optional - -class DecryptionResult(sessionKey: SessionKey?, val verifications: List) { - val sessionKey: Optional - - init { - this.sessionKey = Optional.ofNullable(sessionKey) - } -} diff --git a/sop-java/src/main/kotlin/sop/EncryptionResult.kt b/sop-java/src/main/kotlin/sop/EncryptionResult.kt deleted file mode 100644 index 1284f89..0000000 --- a/sop-java/src/main/kotlin/sop/EncryptionResult.kt +++ /dev/null @@ -1,15 +0,0 @@ -// 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/MicAlg.kt b/sop-java/src/main/kotlin/sop/MicAlg.kt deleted file mode 100644 index 58ce7b5..0000000 --- a/sop-java/src/main/kotlin/sop/MicAlg.kt +++ /dev/null @@ -1,34 +0,0 @@ -// 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/main/kotlin/sop/Profile.kt b/sop-java/src/main/kotlin/sop/Profile.kt deleted file mode 100644 index fd58c63..0000000 --- a/sop-java/src/main/kotlin/sop/Profile.kt +++ /dev/null @@ -1,124 +0,0 @@ -// 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. - * @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, - val aliases: List = listOf() -) { - - @JvmOverloads - constructor( - name: String, - description: String? = null, - aliases: List = listOf() - ) : this(name, Optional.ofNullable(description?.trim()?.ifBlank { null }), aliases) - - 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 - - /** - * 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. - * - * @return string - */ - 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 { - - /** - * Parse a [Profile] from its string representation. - * - * @param string string representation - * @return profile - */ - @JvmStatic - fun parse(string: String): Profile { - return if (string.contains(": ")) { - 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, description, aliases.split(", ").toList()) - } else { - if (description.isNotBlank()) { - Profile(name, description) - } else { - Profile(name) - } - } - } 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 - } -} diff --git a/sop-java/src/main/kotlin/sop/Ready.kt b/sop-java/src/main/kotlin/sop/Ready.kt deleted file mode 100644 index 1eb3fb3..0000000 --- a/sop-java/src/main/kotlin/sop/Ready.kt +++ /dev/null @@ -1,49 +0,0 @@ -// 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) -} diff --git a/sop-java/src/main/kotlin/sop/ReadyWithResult.kt b/sop-java/src/main/kotlin/sop/ReadyWithResult.kt deleted file mode 100644 index d309c76..0000000 --- a/sop-java/src/main/kotlin/sop/ReadyWithResult.kt +++ /dev/null @@ -1,41 +0,0 @@ -// 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) - } -} diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt deleted file mode 100644 index a942c56..0000000 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop - -import sop.operation.* - -/** - * 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 : SOPV { - - /** 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? - - /** 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? - - /** Keep a secret key up-to-date. */ - fun updateKey(): UpdateKey? - - /** Merge OpenPGP certificates. */ - fun mergeCerts(): MergeCerts? - - /** Certify OpenPGP Certificate User-IDs. */ - fun certifyUserId(): CertifyUserId? -} diff --git a/sop-java/src/main/kotlin/sop/SOPV.kt b/sop-java/src/main/kotlin/sop/SOPV.kt deleted file mode 100644 index d483194..0000000 --- a/sop-java/src/main/kotlin/sop/SOPV.kt +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -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. */ -interface SOPV { - - /** - * 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. - * - * @since sopv 1.2 - */ - fun validateUserId(): ValidateUserId? -} diff --git a/sop-java/src/main/kotlin/sop/SessionKey.kt b/sop-java/src/main/kotlin/sop/SessionKey.kt deleted file mode 100644 index af230f3..0000000 --- a/sop-java/src/main/kotlin/sop/SessionKey.kt +++ /dev/null @@ -1,48 +0,0 @@ -// 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))) - } - } -} diff --git a/sop-java/src/main/kotlin/sop/Signatures.kt b/sop-java/src/main/kotlin/sop/Signatures.kt deleted file mode 100644 index 63aafe9..0000000 --- a/sop-java/src/main/kotlin/sop/Signatures.kt +++ /dev/null @@ -1,19 +0,0 @@ -// 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) -} diff --git a/sop-java/src/main/kotlin/sop/SigningResult.kt b/sop-java/src/main/kotlin/sop/SigningResult.kt deleted file mode 100644 index 651f8c1..0000000 --- a/sop-java/src/main/kotlin/sop/SigningResult.kt +++ /dev/null @@ -1,30 +0,0 @@ -// 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](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) { - - 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() - } -} diff --git a/sop-java/src/main/kotlin/sop/Verification.kt b/sop-java/src/main/kotlin/sop/Verification.kt deleted file mode 100644 index 982e691..0000000 --- a/sop-java/src/main/kotlin/sop/Verification.kt +++ /dev/null @@ -1,171 +0,0 @@ -// 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 - -/** - * 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, - val signingCertFingerprint: String, - val signatureMode: Optional, - val jsonOrDescription: 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())) - - @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 - - /** 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. - * - * @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 "") + - (if (jsonOrDescription.isPresent) " ${jsonOrDescription.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) - } - } - } - - /** - * 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?) { - - /** 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]. - * - * @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 - } -} diff --git a/sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt b/sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt deleted file mode 100644 index 3440edf..0000000 --- a/sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt +++ /dev/null @@ -1,17 +0,0 @@ -// 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 -} diff --git a/sop-java/src/main/kotlin/sop/enums/SignAs.kt b/sop-java/src/main/kotlin/sop/enums/SignAs.kt deleted file mode 100644 index 832e831..0000000 --- a/sop-java/src/main/kotlin/sop/enums/SignAs.kt +++ /dev/null @@ -1,12 +0,0 @@ -// 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/main/kotlin/sop/enums/SignatureMode.kt b/sop-java/src/main/kotlin/sop/enums/SignatureMode.kt deleted file mode 100644 index 7213195..0000000 --- a/sop-java/src/main/kotlin/sop/enums/SignatureMode.kt +++ /dev/null @@ -1,18 +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 - */ -enum class SignatureMode { - /** Signature of a binary document (type `0x00`). */ - binary, - /** Signature of a canonical text document (type `0x01`). */ - text -} diff --git a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt deleted file mode 100644 index 9df1628..0000000 --- a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt +++ /dev/null @@ -1,416 +0,0 @@ -// 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 - - /** 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 - 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 - } - } - - /** - * 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 - } - } - - /** 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 { - - 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 - - companion object { - 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 - } - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt b/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt deleted file mode 100644 index 72b8f72..0000000 --- a/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt +++ /dev/null @@ -1,88 +0,0 @@ -// 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(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)) - - /** - * 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 -} diff --git a/sop-java/src/main/kotlin/sop/operation/AbstractVerify.kt b/sop-java/src/main/kotlin/sop/operation/AbstractVerify.kt deleted file mode 100644 index 3554ea3..0000000 --- a/sop-java/src/main/kotlin/sop/operation/AbstractVerify.kt +++ /dev/null @@ -1,57 +0,0 @@ -// 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()) -} diff --git a/sop-java/src/main/kotlin/sop/operation/Armor.kt b/sop-java/src/main/kotlin/sop/operation/Armor.kt deleted file mode 100644 index b54aed7..0000000 --- a/sop-java/src/main/kotlin/sop/operation/Armor.kt +++ /dev/null @@ -1,35 +0,0 @@ -// 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 - -/** Interface for armoring binary OpenPGP data. */ -interface 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()) -} diff --git a/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt deleted file mode 100644 index d59f9f0..0000000 --- a/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-FileCopyrightText: 2024 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.* -import sop.util.UTF8Util - -/** Interface for issuing certifications over UserIDs on certificates. */ -interface CertifyUserId { - - /** Disable ASCII armor for the output. */ - @Throws(UnsupportedOption::class) fun noArmor(): 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 - - /** - * 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)) - - /** - * 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 - - /** - * 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 - - /** - * 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 - - /** - * 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()) - - /** - * 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 - - /** - * 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 deleted file mode 100644 index fe9b8c9..0000000 --- a/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt +++ /dev/null @@ -1,120 +0,0 @@ -// 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 for changing key passwords. */ -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 - */ - fun oldKeyPassphrase(oldPassphrase: CharArray): ChangeKeyPassword = - oldKeyPassphrase(oldPassphrase.concatToString()) - - /** - * 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 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 if the old key passphrase is not human-readable - */ - @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 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 - * not called, the key material will not be protected. - * - * @param newPassphrase new passphrase - * @return builder instance - */ - 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. - * - * @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 -} diff --git a/sop-java/src/main/kotlin/sop/operation/Dearmor.kt b/sop-java/src/main/kotlin/sop/operation/Dearmor.kt deleted file mode 100644 index 2984f27..0000000 --- a/sop-java/src/main/kotlin/sop/operation/Dearmor.kt +++ /dev/null @@ -1,47 +0,0 @@ -// 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 for removing ASCII armor from OpenPGP data. */ -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)) -} diff --git a/sop-java/src/main/kotlin/sop/operation/Decrypt.kt b/sop-java/src/main/kotlin/sop/operation/Decrypt.kt deleted file mode 100644 index 4d009f9..0000000 --- a/sop-java/src/main/kotlin/sop/operation/Decrypt.kt +++ /dev/null @@ -1,166 +0,0 @@ -// 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 for decrypting encrypted OpenPGP messages. */ -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 = 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 = - 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 = - ciphertext(ciphertext.inputStream()) -} diff --git a/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt b/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt deleted file mode 100644 index 4aaadc1..0000000 --- a/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt +++ /dev/null @@ -1,51 +0,0 @@ -// 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 for creating detached signatures over plaintext messages. */ -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()) -} diff --git a/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt b/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt deleted file mode 100644 index 319658d..0000000 --- a/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt +++ /dev/null @@ -1,35 +0,0 @@ -// 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 - -/** Interface for verifying detached OpenPGP signatures over plaintext messages. */ -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(signatures.inputStream()) -} diff --git a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt deleted file mode 100644 index 02c7f97..0000000 --- a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt +++ /dev/null @@ -1,169 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation - -import java.io.IOException -import java.io.InputStream -import sop.EncryptionResult -import sop.Profile -import sop.ReadyWithResult -import sop.enums.EncryptAs -import sop.exception.SOPGPException.* -import sop.util.UTF8Util - -/** Interface for creating encrypted OpenPGP messages. */ -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(key.inputStream()) - - /** - * 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(cert.inputStream()) - - /** - * 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 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): ReadyWithResult - - /** - * Encrypt the given data yielding the ciphertext. - * - * @param plaintext plaintext - * @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): ReadyWithResult = - 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 deleted file mode 100644 index 6485bc2..0000000 --- a/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt +++ /dev/null @@ -1,42 +0,0 @@ -// 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 - -/** Interface for extracting certificates from OpenPGP keys. */ -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(key.inputStream()) -} diff --git a/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt b/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt deleted file mode 100644 index bccd372..0000000 --- a/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt +++ /dev/null @@ -1,92 +0,0 @@ -// 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 for generating OpenPGP keys. */ -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 [java.io.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 -} diff --git a/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt b/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt deleted file mode 100644 index 1cc64ce..0000000 --- a/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt +++ /dev/null @@ -1,44 +0,0 @@ -// 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 for detaching inline signatures from OpenPGP messages. */ -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()) -} diff --git a/sop-java/src/main/kotlin/sop/operation/InlineSign.kt b/sop-java/src/main/kotlin/sop/operation/InlineSign.kt deleted file mode 100644 index 6855a61..0000000 --- a/sop-java/src/main/kotlin/sop/operation/InlineSign.kt +++ /dev/null @@ -1,48 +0,0 @@ -// 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 for creating inline-signed OpenPGP messages. */ -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()) -} diff --git a/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt b/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt deleted file mode 100644 index a944957..0000000 --- a/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt +++ /dev/null @@ -1,42 +0,0 @@ -// 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 - -/** Interface 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()) -} diff --git a/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt b/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt deleted file mode 100644 index 0bed1f8..0000000 --- a/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation - -import sop.Profile - -/** Interface 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") -} diff --git a/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt deleted file mode 100644 index 20469cb..0000000 --- a/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-FileCopyrightText: 2024 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.* - -/** Interface for merging multiple copies of the same certificate into one. */ -interface 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 - - /** - * 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 - - /** - * 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()) - - /** - * 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 - - /** - * 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 deleted file mode 100644 index 13c6712..0000000 --- a/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt +++ /dev/null @@ -1,78 +0,0 @@ -// 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.* -import sop.util.UTF8Util - -/** Interface for creating certificate revocations. */ -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(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)) - - /** - * 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 - - /** - * 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 deleted file mode 100644 index 13a4bdc..0000000 --- a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt +++ /dev/null @@ -1,124 +0,0 @@ -// SPDX-FileCopyrightText: 2024 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.* -import sop.util.UTF8Util - -/** Interface for bringing an OpenPGP key up to date. */ -interface UpdateKey { - - /** - * Disable ASCII armor encoding of the output. - * - * @return builder instance - */ - 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. - * - * @return builder instance - * @throws UnsupportedOption if this option is not supported - */ - @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(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(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)) - - /** - * 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(PasswordNotHumanReadable::class, 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 - * @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(UnsupportedOption::class, 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 - * @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(UnsupportedOption::class, 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 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(BadData::class, IOException::class, KeyIsProtected::class, 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 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(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 deleted file mode 100644 index 971de25..0000000 --- a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-FileCopyrightText: 2024 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.* - -/** Interface 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 builder instance - * @throws UnsupportedOption if this option is not supported - */ - @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 builder instance - */ - 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 builder instance - * @throws BadData if the authority certificates cannot be read - * @throws IOException if an IO error occurs - */ - @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 builder instance - * @throws BadData if the authority certificates cannot be read - * @throws IOException if an IO error occurs - */ - @Throws(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 BadData if the subject certificates are malformed - * @throws IOException if a parser exception happens - * @throws CertUserIdNoMatch if any subject certificate does not have a correctly bound UserID - * that matches [userId]. - */ - @Throws(BadData::class, IOException::class, 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 BadData if the subject certificates are malformed - * @throws IOException if a parser exception happens - * @throws CertUserIdNoMatch if any subject certificate does not have a correctly bound UserID - * that matches [userId]. - */ - @Throws(BadData::class, IOException::class, CertUserIdNoMatch::class) - fun subjects(certs: ByteArray): Boolean = subjects(certs.inputStream()) - - /** - * 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 deleted file mode 100644 index 00a64aa..0000000 --- a/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt +++ /dev/null @@ -1,39 +0,0 @@ -// 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 - -/** API handle for verifying signatures. */ -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()) -} diff --git a/sop-java/src/main/kotlin/sop/operation/Version.kt b/sop-java/src/main/kotlin/sop/operation/Version.kt deleted file mode 100644 index 8a4c808..0000000 --- a/sop-java/src/main/kotlin/sop/operation/Version.kt +++ /dev/null @@ -1,127 +0,0 @@ -// 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 kotlin.jvm.Throws -import sop.exception.SOPGPException - -/** Interface for acquiring version information about the SOP implementation. */ -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? - - /** - * 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 - - /** Return the current version of the SOP-Java library. */ - fun getSopJavaVersion(): String? { - return try { - val resourceIn: InputStream = - 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) { - "DEVELOPMENT" - } - } -} diff --git a/sop-java/src/main/kotlin/sop/util/HexUtil.kt b/sop-java/src/main/kotlin/sop/util/HexUtil.kt deleted file mode 100644 index f372137..0000000 --- a/sop-java/src/main/kotlin/sop/util/HexUtil.kt +++ /dev/null @@ -1,38 +0,0 @@ -// 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) } diff --git a/sop-java/src/main/kotlin/sop/util/Optional.kt b/sop-java/src/main/kotlin/sop/util/Optional.kt deleted file mode 100644 index 0344d0b..0000000 --- a/sop-java/src/main/kotlin/sop/util/Optional.kt +++ /dev/null @@ -1,26 +0,0 @@ -// 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?) - } -} diff --git a/sop-java/src/main/kotlin/sop/util/UTCUtil.kt b/sop-java/src/main/kotlin/sop/util/UTCUtil.kt deleted file mode 100644 index 4ae13bc..0000000 --- a/sop-java/src/main/kotlin/sop/util/UTCUtil.kt +++ /dev/null @@ -1,62 +0,0 @@ -// 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 { - - @JvmField val UTC_FORMATTER = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") - @JvmField - 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) - } - } -} diff --git a/sop-java/src/main/kotlin/sop/util/UTF8Util.kt b/sop-java/src/main/kotlin/sop/util/UTF8Util.kt deleted file mode 100644 index 770f32c..0000000 --- a/sop-java/src/main/kotlin/sop/util/UTF8Util.kt +++ /dev/null @@ -1,37 +0,0 @@ -// 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() - } - } -} diff --git a/sop-java/src/main/resources/sop-java-version.properties b/sop-java/src/main/resources/sop-java-version.properties deleted file mode 100644 index a2f509b..0000000 --- a/sop-java/src/main/resources/sop-java-version.properties +++ /dev/null @@ -1,4 +0,0 @@ -# SPDX-FileCopyrightText: 2025 Paul Schaub -# -# SPDX-License-Identifier: Apache-2.0 -sop-java-version=@project.version@ \ No newline at end of file diff --git a/sop-java/src/test/java/sop/MicAlgTest.java b/sop-java/src/test/java/sop/MicAlgTest.java index 7c6b30a..16f54ef 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(NullPointerException.class, () -> new MicAlg(null)); + assertThrows(IllegalArgumentException.class, () -> new MicAlg(null)); } @Test diff --git a/sop-java/src/test/java/sop/ProfileTest.java b/sop-java/src/test/java/sop/ProfileTest.java index f418672..564a6af 100644 --- a/sop-java/src/test/java/sop/ProfileTest.java +++ b/sop-java/src/test/java/sop/ProfileTest.java @@ -5,9 +5,6 @@ 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; @@ -22,37 +19,6 @@ 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 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"); diff --git a/sop-java/src/test/java/sop/VerificationJSONTest.java b/sop-java/src/test/java/sop/VerificationJSONTest.java deleted file mode 100644 index a6e5d96..0000000 --- a/sop-java/src/test/java/sop/VerificationJSONTest.java +++ /dev/null @@ -1,166 +0,0 @@ -// 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 sop.testsuite.assertions.VerificationAssert; - -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); - VerificationAssert.assertThatVerification(verification) - .hasJSON(dummyParser, j -> j.getSigners().contains("alice.pgp")); - 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 1e10f61..e956435 100644 --- a/sop-java/src/test/java/sop/VerificationTest.java +++ b/sop-java/src/test/java/sop/VerificationTest.java @@ -13,7 +13,6 @@ 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 { @@ -26,8 +25,6 @@ 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) diff --git a/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java b/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java new file mode 100644 index 0000000..9d99fd4 --- /dev/null +++ b/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java @@ -0,0 +1,40 @@ +// 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()); + } +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/JUtils.java b/sop-java/src/testFixtures/java/sop/testsuite/JUtils.java similarity index 100% rename from sop-java-testfixtures/src/main/java/sop/testsuite/JUtils.java rename to sop-java/src/testFixtures/java/sop/testsuite/JUtils.java diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/SOPInstanceFactory.java b/sop-java/src/testFixtures/java/sop/testsuite/SOPInstanceFactory.java similarity index 100% rename from sop-java-testfixtures/src/main/java/sop/testsuite/SOPInstanceFactory.java rename to sop-java/src/testFixtures/java/sop/testsuite/SOPInstanceFactory.java diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/TestData.java b/sop-java/src/testFixtures/java/sop/testsuite/TestData.java similarity index 100% rename from sop-java-testfixtures/src/main/java/sop/testsuite/TestData.java rename to sop-java/src/testFixtures/java/sop/testsuite/TestData.java diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java b/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationAssert.java similarity index 62% rename from sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java rename to sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationAssert.java index dea8717..63fd237 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationAssert.java @@ -8,13 +8,9 @@ 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 { @@ -49,39 +45,18 @@ public final class VerificationAssert { } public VerificationAssert hasDescription(String description) { - assertEquals(description, verification.getJsonOrDescription().get()); + assertEquals(description, verification.getDescription().get()); return this; } public VerificationAssert hasDescriptionOrNull(String description) { - if (verification.getJsonOrDescription().isEmpty()) { + if (verification.getDescription().isEmpty()) { return this; } 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/assertions/VerificationListAssert.java b/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationListAssert.java similarity index 100% rename from sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationListAssert.java rename to sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationListAssert.java diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/package-info.java b/sop-java/src/testFixtures/java/sop/testsuite/assertions/package-info.java similarity index 100% rename from sop-java-testfixtures/src/main/java/sop/testsuite/assertions/package-info.java rename to sop-java/src/testFixtures/java/sop/testsuite/assertions/package-info.java diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java similarity index 69% rename from sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java rename to sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java index 16ae256..8595898 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java @@ -4,15 +4,9 @@ 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; import java.lang.reflect.InvocationTargetException; @@ -21,10 +15,6 @@ 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 { private static final List backends = new ArrayList<>(); @@ -56,17 +46,6 @@ 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/src/testFixtures/java/sop/testsuite/operation/ArmorDearmorTest.java similarity index 88% rename from sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java rename to sop-java/src/testFixtures/java/sop/testsuite/operation/ArmorDearmorTest.java index 00488e1..35959b0 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java +++ b/sop-java/src/testFixtures/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 extends AbstractSOPTest { +public class ArmorDearmorTest { static Stream provideInstances() { return AbstractSOPTest.provideBackends(); @@ -31,13 +31,13 @@ public class ArmorDearmorTest extends AbstractSOPTest { public void dearmorArmorAliceKey(SOP sop) throws IOException { byte[] aliceKey = TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = assumeSupported(sop::dearmor) + byte[] dearmored = sop.dearmor() .data(aliceKey) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK)); - byte[] armored = assumeSupported(sop::armor) + byte[] armored = sop.armor() .data(dearmored) .getBytes(); @@ -52,13 +52,13 @@ public class ArmorDearmorTest extends AbstractSOPTest { public void dearmorArmorAliceCert(SOP sop) throws IOException { byte[] aliceCert = TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = assumeSupported(sop::dearmor) + byte[] dearmored = sop.dearmor() .data(aliceCert) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); - byte[] armored = assumeSupported(sop::armor) + byte[] armored = sop.armor() .data(dearmored) .getBytes(); @@ -73,13 +73,13 @@ public class ArmorDearmorTest extends AbstractSOPTest { public void dearmorArmorBobKey(SOP sop) throws IOException { byte[] bobKey = TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = assumeSupported(sop::dearmor) + byte[] dearmored = sop.dearmor() .data(bobKey) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK)); - byte[] armored = assumeSupported(sop::armor) + byte[] armored = sop.armor() .data(dearmored) .getBytes(); @@ -94,13 +94,13 @@ public class ArmorDearmorTest extends AbstractSOPTest { public void dearmorArmorBobCert(SOP sop) throws IOException { byte[] bobCert = TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = assumeSupported(sop::dearmor) + byte[] dearmored = sop.dearmor() .data(bobCert) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); - byte[] armored = assumeSupported(sop::armor) + byte[] armored = sop.armor() .data(dearmored) .getBytes(); @@ -115,13 +115,13 @@ public class ArmorDearmorTest extends AbstractSOPTest { public void dearmorArmorCarolKey(SOP sop) throws IOException { byte[] carolKey = TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = assumeSupported(sop::dearmor) + byte[] dearmored = sop.dearmor() .data(carolKey) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK)); - byte[] armored = assumeSupported(sop::armor) + byte[] armored = sop.armor() .data(dearmored) .getBytes(); @@ -136,13 +136,13 @@ public class ArmorDearmorTest extends AbstractSOPTest { public void dearmorArmorCarolCert(SOP sop) throws IOException { byte[] carolCert = TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = assumeSupported(sop::dearmor) + byte[] dearmored = sop.dearmor() .data(carolCert) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); - byte[] armored = assumeSupported(sop::armor) + byte[] armored = sop.armor() .data(dearmored) .getBytes(); @@ -163,13 +163,13 @@ public class ArmorDearmorTest extends AbstractSOPTest { "CePQFpprprnGEzpE3flQLUc=\n" + "=ZiFR\n" + "-----END PGP MESSAGE-----\n").getBytes(StandardCharsets.UTF_8); - byte[] dearmored = assumeSupported(sop::dearmor) + byte[] dearmored = sop.dearmor() .data(message) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_MESSAGE)); - byte[] armored = assumeSupported(sop::armor) + byte[] armored = sop.armor() .data(dearmored) .getBytes(); @@ -191,13 +191,13 @@ public class ArmorDearmorTest extends AbstractSOPTest { "=GHvQ\n" + "-----END PGP SIGNATURE-----\n").getBytes(StandardCharsets.UTF_8); - byte[] dearmored = assumeSupported(sop::dearmor) + byte[] dearmored = sop.dearmor() .data(signature) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_SIGNATURE)); - byte[] armored = assumeSupported(sop::armor) + byte[] armored = sop.armor() .data(dearmored) .getBytes(); @@ -210,11 +210,11 @@ public class ArmorDearmorTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testDearmoringTwiceIsIdempotent(SOP sop) throws IOException { - byte[] dearmored = assumeSupported(sop::dearmor) + byte[] dearmored = sop.dearmor() .data(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .getBytes(); - byte[] dearmoredAgain = assumeSupported(sop::dearmor) + byte[] dearmoredAgain = sop.dearmor() .data(dearmored) .getBytes(); @@ -233,7 +233,7 @@ public class ArmorDearmorTest extends AbstractSOPTest { "=GHvQ\n" + "-----END PGP SIGNATURE-----\n").getBytes(StandardCharsets.UTF_8); - byte[] armoredAgain = assumeSupported(sop::armor) + byte[] armoredAgain = sop.armor() .data(armored) .getBytes(); diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java similarity index 51% rename from sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java rename to sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java index a62cbb8..b575047 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java @@ -10,8 +10,6 @@ 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; @@ -19,7 +17,6 @@ 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") @@ -32,18 +29,18 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void changePasswordFromUnprotectedToProtected(SOP sop) throws IOException { - byte[] unprotectedKey = assumeSupported(sop::generateKey).generate().getBytes(); + byte[] unprotectedKey = sop.generateKey().generate().getBytes(); byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); - byte[] protectedKey = assumeSupported(sop::changeKeyPassword).newKeyPassphrase(password).keys(unprotectedKey).getBytes(); + byte[] protectedKey = sop.changeKeyPassword().newKeyPassphrase(password).keys(unprotectedKey).getBytes(); - assumeSupported(sop::sign).withKeyPassword(password).key(protectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); + sop.sign().withKeyPassword(password).key(protectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); } @ParameterizedTest @MethodSource("provideInstances") public void changePasswordFromUnprotectedToUnprotected(SOP sop) throws IOException { - byte[] unprotectedKey = assumeSupported(sop::generateKey).noArmor().generate().getBytes(); - byte[] stillUnprotectedKey = assumeSupported(sop::changeKeyPassword).noArmor().keys(unprotectedKey).getBytes(); + byte[] unprotectedKey = sop.generateKey().noArmor().generate().getBytes(); + byte[] stillUnprotectedKey = sop.changeKeyPassword().noArmor().keys(unprotectedKey).getBytes(); assertArrayEquals(unprotectedKey, stillUnprotectedKey); } @@ -52,12 +49,12 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { @MethodSource("provideInstances") public void changePasswordFromProtectedToUnprotected(SOP sop) throws IOException { byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); - byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(password).generate().getBytes(); - byte[] unprotectedKey = assumeSupported(sop::changeKeyPassword) + byte[] protectedKey = sop.generateKey().withKeyPassword(password).generate().getBytes(); + byte[] unprotectedKey = sop.changeKeyPassword() .oldKeyPassphrase(password) .keys(protectedKey).getBytes(); - assumeSupported(sop::sign).key(unprotectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); + sop.sign().key(unprotectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); } @ParameterizedTest @@ -65,13 +62,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 = assumeSupported(sop::generateKey).withKeyPassword(oldPassword).generate().getBytes(); - byte[] reprotectedKey = assumeSupported(sop::changeKeyPassword) + byte[] protectedKey = sop.generateKey().withKeyPassword(oldPassword).generate().getBytes(); + byte[] reprotectedKey = sop.changeKeyPassword() .oldKeyPassphrase(oldPassword) .newKeyPassphrase(newPassword) .keys(protectedKey).getBytes(); - assumeSupported(sop::sign).key(reprotectedKey).withKeyPassword(newPassword).data("Test123".getBytes(StandardCharsets.UTF_8)); + sop.sign().key(reprotectedKey).withKeyPassword(newPassword).data("Test123".getBytes(StandardCharsets.UTF_8)); } @@ -82,8 +79,8 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { byte[] newPassword = "monkey123".getBytes(UTF8Util.UTF8); byte[] wrongPassword = "0r4ng3".getBytes(UTF8Util.UTF8); - byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(oldPassword).generate().getBytes(); - assertThrows(SOPGPException.KeyIsProtected.class, () -> assumeSupported(sop::changeKeyPassword) + byte[] protectedKey = sop.generateKey().withKeyPassword(oldPassword).generate().getBytes(); + assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.changeKeyPassword() .oldKeyPassphrase(wrongPassword) .newKeyPassphrase(newPassword) .keys(protectedKey).getBytes()); @@ -93,32 +90,9 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { @MethodSource("provideInstances") public void nonUtf8PasswordsFail(SOP sop) { assertThrows(SOPGPException.PasswordNotHumanReadable.class, () -> - assumeSupported(sop::changeKeyPassword).oldKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); + sop.changeKeyPassword().oldKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); assertThrows(SOPGPException.PasswordNotHumanReadable.class, () -> - assumeSupported(sop::changeKeyPassword).newKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); + 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 = assumeSupported(sop::generateKey).withKeyPassword(oldPassword).generate().getBytes(); - - byte[] armored = assumeSupported(sop::changeKeyPassword) - .oldKeyPassphrase(oldPassword) - .newKeyPassphrase(newPassword) - .keys(protectedKey) - .getBytes(); - JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); - - byte[] unarmored = assumeSupported(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-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java similarity index 94% rename from sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java rename to sop-java/src/testFixtures/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java index 8fd201a..65ec4a5 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java +++ b/sop-java/src/testFixtures/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 = assumeSupported(sop::decrypt) + ByteArrayAndResult bytesAndResult = 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 = assumeSupported(sop::decrypt) + byte[] decrypted = 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/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java similarity index 86% rename from sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java rename to sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java index 415b9db..e715c14 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java +++ b/sop-java/src/testFixtures/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 = assumeSupported(sop::detachedSign) + byte[] signature = sop.detachedSign() .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult() .getBytes(); - List verificationList = assumeSupported(sop::detachedVerify) + List verificationList = 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 = assumeSupported(sop::detachedSign) + byte[] signature = sop.detachedSign() .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) - .mode(SignAs.text) + .mode(SignAs.Text) .data(message) .toByteArrayAndResult() .getBytes(); - List verificationList = assumeSupported(sop::detachedVerify) + List verificationList = 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 = assumeSupported(sop::detachedVerify) + List verificationList = 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 = assumeSupported(sop::detachedSign) + byte[] signature = sop.detachedSign() .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult() .getBytes(); - List verificationList = assumeSupported(sop::detachedVerify) + List verificationList = 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 = assumeSupported(sop::detachedSign) + byte[] signature = sop.detachedSign() .key(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult() .getBytes(); - List verificationList = assumeSupported(sop::detachedVerify) + List verificationList = 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 = assumeSupported(sop::detachedSign) + byte[] signature = 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 = assumeSupported(sop::detachedVerify) + List verificationList = 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 = assumeSupported(sop::detachedSign) + byte[] signature = sop.detachedSign() .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .noArmor() .data(message) .toByteArrayAndResult() .getBytes(); - byte[] armored = assumeSupported(sop::armor) + byte[] armored = sop.armor() .data(signature) .getBytes(); - List verificationList = assumeSupported(sop::detachedVerify) + List verificationList = 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, () -> assumeSupported(sop::detachedVerify) + assertThrows(SOPGPException.NoSignature.class, () -> 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, () -> assumeSupported(sop::detachedVerify) + assertThrows(SOPGPException.NoSignature.class, () -> 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 = assumeSupported(sop::detachedSign) + byte[] signatures = sop.detachedSign() .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult() .getBytes(); - assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::detachedVerify) + assertThrows(SOPGPException.NoSignature.class, () -> 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, () -> - assumeSupported(sop::detachedSign) + 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 = assumeSupported(sop::sign) + byte[] signature = 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 = assumeSupported(sop::verify) + List verificationList = 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, () -> - assumeSupported(sop::verify) + 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 = assumeSupported(sop::detachedSign) + 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 = assumeSupported(sop::detachedVerify) + List verificationList = 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/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java similarity index 65% rename from sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java rename to sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java index 937b5b7..0c382dc 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java @@ -10,31 +10,23 @@ 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.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; 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; 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; @@ -49,140 +41,122 @@ public class EncryptDecryptTest extends AbstractSOPTest { @MethodSource("provideInstances") public void encryptDecryptRoundTripPasswordTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - ByteArrayAndResult encResult = assumeSupported(sop::encrypt) + byte[] ciphertext = sop.encrypt() .withPassword("sw0rdf1sh") .plaintext(message) - .toByteArrayAndResult(); + .getBytes(); - byte[] ciphertext = encResult.getBytes(); - Optional encSessionKey = encResult.getResult().getSessionKey(); - - ByteArrayAndResult decResult = assumeSupported(sop::decrypt) + byte[] plaintext = sop.decrypt() .withPassword("sw0rdf1sh") .ciphertext(ciphertext) - .toByteArrayAndResult(); + .toByteArrayAndResult() + .getBytes(); - byte[] plaintext = decResult.getBytes(); - Optional decSessionKey = decResult.getResult().getSessionKey(); - - assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); - if (encSessionKey.isPresent() && decSessionKey.isPresent()) { - assertEquals(encSessionKey.get(), decSessionKey.get(), - "Extracted Session Key mismatch."); - } + assertArrayEquals(message, plaintext); } @ParameterizedTest @MethodSource("provideInstances") public void encryptDecryptRoundTripAliceTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = assumeSupported(sop::encrypt) + byte[] ciphertext = sop.encrypt() .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .plaintext(message) - .toByteArrayAndResult() .getBytes(); - ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) + ByteArrayAndResult bytesAndResult = sop.decrypt() .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .ciphertext(ciphertext) .toByteArrayAndResult(); byte[] plaintext = bytesAndResult.getBytes(); - assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); + assertArrayEquals(message, plaintext); DecryptionResult result = bytesAndResult.getResult(); - if (result.getSessionKey().isPresent()) { - assertNotNull(result.getSessionKey().get(), "Session key MUST NOT be null."); - } + assertNotNull(result.getSessionKey().get()); } @ParameterizedTest @MethodSource("provideInstances") public void encryptDecryptRoundTripBobTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = assumeSupported(sop::encrypt) + byte[] ciphertext = sop.encrypt() .withCert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .plaintext(message) - .toByteArrayAndResult() .getBytes(); - byte[] plaintext = assumeSupported(sop::decrypt) + byte[] plaintext = sop.decrypt() .withKey(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .ciphertext(ciphertext) .toByteArrayAndResult() .getBytes(); - assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); + assertArrayEquals(message, plaintext); } @ParameterizedTest @MethodSource("provideInstances") public void encryptDecryptRoundTripCarolTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = assumeSupported(sop::encrypt) + byte[] ciphertext = sop.encrypt() .withCert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8)) .plaintext(message) - .toByteArrayAndResult() .getBytes(); - byte[] plaintext = assumeSupported(sop::decrypt) + byte[] plaintext = sop.decrypt() .withKey(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8)) .ciphertext(ciphertext) .toByteArrayAndResult() .getBytes(); - assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); + assertArrayEquals(message, plaintext); } @ParameterizedTest @MethodSource("provideInstances") public void encryptNoArmorThenArmorThenDecryptRoundTrip(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = assumeSupported(sop::encrypt) + byte[] ciphertext = sop.encrypt() .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .noArmor() .plaintext(message) - .toByteArrayAndResult() .getBytes(); - byte[] armored = assumeSupported(sop::armor) + byte[] armored = sop.armor() .data(ciphertext) .getBytes(); - ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) + ByteArrayAndResult bytesAndResult = sop.decrypt() .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .ciphertext(armored) .toByteArrayAndResult(); byte[] plaintext = bytesAndResult.getBytes(); - assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); + assertArrayEquals(message, plaintext); } @ParameterizedTest @MethodSource("provideInstances") public void encryptSignDecryptVerifyRoundTripAliceTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = assumeSupported(sop::encrypt) + 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) - .toByteArrayAndResult() .getBytes(); - ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) + ByteArrayAndResult bytesAndResult = 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, "Decrypted plaintext does not match original plaintext."); + assertArrayEquals(message, plaintext); DecryptionResult result = bytesAndResult.getResult(); - if (result.getSessionKey().isPresent()) { - assertNotNull(result.getSessionKey().get(), "Session key MUST NOT be null."); - } + assertNotNull(result.getSessionKey().get()); List verificationList = result.getVerifications(); VerificationListAssert.assertThatVerificationList(verificationList) @@ -196,22 +170,21 @@ public class EncryptDecryptTest extends AbstractSOPTest { @MethodSource("provideInstances") public void encryptSignAsTextDecryptVerifyRoundTripAliceTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = assumeSupported(sop::encrypt) + 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) - .toByteArrayAndResult() .getBytes(); - ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) + ByteArrayAndResult bytesAndResult = 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, "Decrypted plaintext does not match original plaintext."); + assertArrayEquals(message, plaintext); DecryptionResult result = bytesAndResult.getResult(); assertNotNull(result.getSessionKey().get()); @@ -227,25 +200,24 @@ public class EncryptDecryptTest extends AbstractSOPTest { @MethodSource("provideInstances") public void encryptSignDecryptVerifyRoundTripWithFreshEncryptedKeyTest(SOP sop) throws IOException { byte[] keyPassword = "sw0rdf1sh".getBytes(StandardCharsets.UTF_8); - byte[] key = assumeSupported(sop::generateKey) + byte[] key = sop.generateKey() .withKeyPassword(keyPassword) .userId("Alice ") .generate() .getBytes(); - byte[] cert = assumeSupported(sop::extractCert) + byte[] cert = sop.extractCert() .key(key) .getBytes(); byte[] message = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = assumeSupported(sop::encrypt) + byte[] ciphertext = sop.encrypt() .withCert(cert) .signWith(key) .withKeyPassword(keyPassword) .plaintext(message) - .toByteArrayAndResult() .getBytes(); - ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) + ByteArrayAndResult bytesAndResult = sop.decrypt() .withKey(key) .withKeyPassword(keyPassword) .verifyWithCert(cert) @@ -278,7 +250,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { Date beforeSignature = new Date(signatureDate.getTime() - 1000); // 1 sec before signing date assertThrows(SOPGPException.NoSignature.class, () -> { - ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) + ByteArrayAndResult bytesAndResult = sop.decrypt() .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .verifyNotAfter(beforeSignature) @@ -312,7 +284,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { Date afterSignature = new Date(signatureDate.getTime() + 1000); // 1 sec after signing date assertThrows(SOPGPException.NoSignature.class, () -> { - ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) + ByteArrayAndResult bytesAndResult = sop.decrypt() .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .verifyNotBefore(afterSignature) @@ -331,71 +303,8 @@ public class EncryptDecryptTest extends AbstractSOPTest { public void missingArgsTest(SOP sop) { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - assertThrows(SOPGPException.MissingArg.class, () -> assumeSupported(sop::encrypt) + assertThrows(SOPGPException.MissingArg.class, () -> sop.encrypt() .plaintext(message) - .toByteArrayAndResult() .getBytes()); } - - @ParameterizedTest - @MethodSource("provideInstances") - public void passingSecretKeysForPublicKeysFails(SOP sop) { - assertThrows(SOPGPException.BadData.class, () -> - assumeSupported(sop::encrypt) - .withCert(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) - .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) - .toByteArrayAndResult() - .getBytes()); - } - - @ParameterizedTest - @MethodSource("provideInstances") - public void encryptDecryptWithAllSupportedKeyGenerationProfiles(SOP sop) throws IOException { - List profiles = assumeSupported(sop::listProfiles).generateKey(); - - List keys = new ArrayList<>(); - List certs = new ArrayList<>(); - for (Profile p : profiles) { - byte[] k = assumeSupported(sop::generateKey) - .profile(p) - .userId(p.getName()) - .generate() - .getBytes(); - keys.add(k); - - byte[] c = assumeSupported(sop::extractCert) - .key(k) - .getBytes(); - certs.add(c); - } - - byte[] plaintext = "Hello, World!\n".getBytes(); - - Encrypt encrypt = assumeSupported(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 = assumeSupported(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, "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/src/testFixtures/java/sop/testsuite/operation/ExtractCertTest.java similarity index 84% rename from sop-java-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java rename to sop-java/src/testFixtures/java/sop/testsuite/operation/ExtractCertTest.java index 94d9927..99acf81 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java +++ b/sop-java/src/testFixtures/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 = assumeSupported(sop::generateKey) + InputStream keyIn = sop.generateKey() .userId("Alice ") .generate() .getInputStream(); - byte[] cert = assumeSupported(sop::extractCert).key(keyIn).getBytes(); + byte[] cert = 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 = assumeSupported(sop::extractCert) + byte[] armoredCert = 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 = assumeSupported(sop::extractCert) + byte[] armoredCert = 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 = assumeSupported(sop::extractCert) + byte[] armoredCert = 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 = assumeSupported(sop::generateKey) + InputStream keyIn = sop.generateKey() .userId("Alice ") .generate() .getInputStream(); - byte[] cert = assumeSupported(sop::extractCert) + byte[] cert = 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 = assumeSupported(sop::generateKey) + InputStream keyIn = sop.generateKey() .userId("Alice ") .noArmor() .generate() .getInputStream(); - byte[] cert = assumeSupported(sop::extractCert) + byte[] cert = 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 = assumeSupported(sop::generateKey) + InputStream keyIn = sop.generateKey() .noArmor() .userId("Alice ") .generate() .getInputStream(); - byte[] cert = assumeSupported(sop::extractCert) + byte[] cert = sop.extractCert() .noArmor() .key(keyIn) .getBytes(); diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/GenerateKeyTest.java similarity index 70% rename from sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java rename to sop-java/src/testFixtures/java/sop/testsuite/operation/GenerateKeyTest.java index 787cf62..4a5da58 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/GenerateKeyTest.java @@ -9,7 +9,6 @@ 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; @@ -17,11 +16,9 @@ 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 { @@ -33,7 +30,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyTest(SOP sop) throws IOException { - byte[] key = assumeSupported(sop::generateKey) + byte[] key = sop.generateKey() .userId("Alice ") .generate() .getBytes(); @@ -45,7 +42,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyNoArmor(SOP sop) throws IOException { - byte[] key = assumeSupported(sop::generateKey) + byte[] key = sop.generateKey() .userId("Alice ") .noArmor() .generate() @@ -57,7 +54,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyWithMultipleUserIdsTest(SOP sop) throws IOException { - byte[] key = assumeSupported(sop::generateKey) + byte[] key = sop.generateKey() .userId("Alice ") .userId("Bob ") .generate() @@ -70,7 +67,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyWithoutUserIdTest(SOP sop) throws IOException { - byte[] key = assumeSupported(sop::generateKey) + byte[] key = sop.generateKey() .generate() .getBytes(); @@ -81,7 +78,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyWithPasswordTest(SOP sop) throws IOException { - byte[] key = assumeSupported(sop::generateKey) + byte[] key = sop.generateKey() .userId("Alice ") .withKeyPassword("sw0rdf1sh") .generate() @@ -94,7 +91,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyWithMultipleUserIdsAndPassword(SOP sop) throws IOException { - byte[] key = assumeSupported(sop::generateKey) + byte[] key = sop.generateKey() .userId("Alice ") .userId("Bob ") .withKeyPassword("sw0rdf1sh") @@ -108,45 +105,17 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateSigningOnlyKey(SOP sop) throws IOException { - byte[] signingOnlyKey = assumeSupported(sop::generateKey) + byte[] signingOnlyKey = sop.generateKey() .signingOnly() .userId("Alice ") .generate() .getBytes(); - byte[] signingOnlyCert = assumeSupported(sop::extractCert) + byte[] signingOnlyCert = sop.extractCert() .key(signingOnlyKey) .getBytes(); assertThrows(SOPGPException.CertCannotEncrypt.class, () -> - assumeSupported(sop::encrypt).withCert(signingOnlyCert) - .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) - .toByteArrayAndResult() - .getBytes()); - } - - @ParameterizedTest - @MethodSource("provideInstances") - public void generateKeyWithSupportedProfiles(SOP sop) throws IOException { - List profiles = assumeSupported(sop::listProfiles) - .generateKey(); - - for (Profile profile : profiles) { - generateKeyWithProfile(sop, profile.getName()); - } - } - - private void generateKeyWithProfile(SOP sop, String profile) throws IOException { - byte[] key; - try { - key = assumeSupported(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); + sop.encrypt().withCert(signingOnlyCert) + .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8))); } } diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java similarity index 85% rename from sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java rename to sop-java/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java index ac043b3..3e20a09 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java +++ b/sop-java/src/testFixtures/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 = assumeSupported(sop::inlineSign) + byte[] inlineSigned = sop.inlineSign() .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .getBytes(); - ByteArrayAndResult bytesAndResult = assumeSupported(sop::inlineDetach) + ByteArrayAndResult bytesAndResult = sop.inlineDetach() .message(inlineSigned) .toByteArrayAndResult(); @@ -51,7 +51,7 @@ public class InlineSignInlineDetachDetachedVerifyTest extends AbstractSOPTest { byte[] signatures = bytesAndResult.getResult() .getBytes(); - List verifications = assumeSupported(sop::detachedVerify) + List verifications = 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 = assumeSupported(sop::inlineSign) + byte[] inlineSigned = sop.inlineSign() .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .getBytes(); - ByteArrayAndResult bytesAndResult = assumeSupported(sop::inlineDetach) + ByteArrayAndResult bytesAndResult = 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 = assumeSupported(sop::armor) + byte[] armored = sop.armor() .data(signatures) .getBytes(); JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_SIGNATURE); - List verifications = assumeSupported(sop::detachedVerify) + List verifications = 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/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java similarity index 90% rename from sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java rename to sop-java/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java index d751ee8..39a26c6 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java +++ b/sop-java/src/testFixtures/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 = assumeSupported(sop::inlineSign) + byte[] inlineSigned = sop.inlineSign() .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .getBytes(); JUtils.assertArrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE); - ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) + ByteArrayAndResult> bytesAndResult = 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 = assumeSupported(sop::inlineSign) + byte[] inlineSigned = 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 = assumeSupported(sop::inlineVerify) + ByteArrayAndResult> bytesAndResult = 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 = assumeSupported(sop::inlineSign) + byte[] clearsigned = sop.inlineSign() .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .mode(InlineSignAs.clearsigned) .data(message) @@ -101,13 +101,12 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { JUtils.assertArrayStartsWith(clearsigned, TestData.BEGIN_PGP_SIGNED_MESSAGE); - ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) + ByteArrayAndResult> bytesAndResult = sop.inlineVerify() .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(clearsigned) .toByteArrayAndResult(); - assertArrayEquals(message, bytesAndResult.getBytes(), - "ASCII armored message does not appear to start with the 'BEGIN PGP SIGNED MESSAGE' header."); + assertArrayEquals(message, bytesAndResult.getBytes()); List verificationList = bytesAndResult.getResult(); VerificationListAssert.assertThatVerificationList(verificationList) @@ -122,7 +121,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 = assumeSupported(sop::inlineVerify) + ByteArrayAndResult> bytesAndResult = sop.inlineVerify() .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult(); @@ -142,7 +141,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, () -> assumeSupported(sop::inlineVerify) + assertThrows(SOPGPException.NoSignature.class, () -> sop.inlineVerify() .notBefore(afterSignature) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(message) @@ -156,7 +155,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, () -> assumeSupported(sop::inlineVerify) + assertThrows(SOPGPException.NoSignature.class, () -> sop.inlineVerify() .notAfter(beforeSignature) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(message) @@ -168,14 +167,14 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { public void inlineSignVerifyBob(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = assumeSupported(sop::inlineSign) + byte[] inlineSigned = sop.inlineSign() .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .getBytes(); JUtils.assertArrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE); - ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) + ByteArrayAndResult> bytesAndResult = sop.inlineVerify() .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .data(inlineSigned) .toByteArrayAndResult(); @@ -194,14 +193,14 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { public void inlineSignVerifyCarol(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = assumeSupported(sop::inlineSign) + byte[] inlineSigned = sop.inlineSign() .key(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .getBytes(); JUtils.assertArrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE); - ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) + ByteArrayAndResult> bytesAndResult = sop.inlineVerify() .cert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8)) .data(inlineSigned) .toByteArrayAndResult(); @@ -220,14 +219,14 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { public void inlineSignVerifyProtectedKey(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = assumeSupported(sop::inlineSign) + byte[] inlineSigned = sop.inlineSign() .withKeyPassword(TestData.PASSWORD) .key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8)) .mode(InlineSignAs.binary) .data(message) .getBytes(); - ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) + ByteArrayAndResult> bytesAndResult = 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/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java similarity index 88% rename from sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java rename to sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java index 4faa1b3..6d3c4c4 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java @@ -26,7 +26,8 @@ public class ListProfilesTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void listGenerateKeyProfiles(SOP sop) { - List profiles = assumeSupported(sop::listProfiles) + List profiles = sop + .listProfiles() .generateKey(); assertFalse(profiles.isEmpty()); @@ -35,7 +36,8 @@ public class ListProfilesTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void listEncryptProfiles(SOP sop) { - List profiles = assumeSupported(sop::listProfiles) + List profiles = sop + .listProfiles() .encrypt(); assertFalse(profiles.isEmpty()); @@ -44,7 +46,8 @@ public class ListProfilesTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void listUnsupportedProfiles(SOP sop) { - assertThrows(SOPGPException.UnsupportedProfile.class, () -> assumeSupported(sop::listProfiles) + assertThrows(SOPGPException.UnsupportedProfile.class, () -> sop + .listProfiles() .subcommand("invalid")); } } diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java similarity index 55% rename from sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java rename to sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java index 1880d58..6595133 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java @@ -36,27 +36,18 @@ public class RevokeKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void revokeUnprotectedKey(SOP sop) throws IOException { - byte[] secretKey = assumeSupported(sop::generateKey).userId("Alice ").generate().getBytes(); - byte[] revocation = assumeSupported(sop::revokeKey).keys(secretKey).getBytes(); + 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 revokeUnprotectedKeyNoArmor(SOP sop) throws IOException { - 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)); - } - @ParameterizedTest @MethodSource("provideInstances") public void revokeUnprotectedKeyUnarmored(SOP sop) throws IOException { - byte[] secretKey = assumeSupported(sop::generateKey).userId("Alice ").noArmor().generate().getBytes(); - byte[] revocation = assumeSupported(sop::revokeKey).noArmor().keys(secretKey).getBytes(); + 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)); @@ -65,18 +56,18 @@ public class RevokeKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void revokeCertificateFails(SOP sop) throws IOException { - byte[] secretKey = assumeSupported(sop::generateKey).generate().getBytes(); - byte[] certificate = assumeSupported(sop::extractCert).key(secretKey).getBytes(); + byte[] secretKey = sop.generateKey().generate().getBytes(); + byte[] certificate = sop.extractCert().key(secretKey).getBytes(); - assertThrows(SOPGPException.BadData.class, () -> assumeSupported(sop::revokeKey).keys(certificate).getBytes()); + assertThrows(SOPGPException.BadData.class, () -> sop.revokeKey().keys(certificate).getBytes()); } @ParameterizedTest @MethodSource("provideInstances") public void revokeProtectedKey(SOP sop) throws IOException { byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); - byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice ").generate().getBytes(); - byte[] revocation = assumeSupported(sop::revokeKey).withKeyPassword(password).keys(secretKey).getBytes(); + byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice ").generate().getBytes(); + byte[] revocation = sop.revokeKey().withKeyPassword(password).keys(secretKey).getBytes(); assertFalse(Arrays.equals(secretKey, revocation)); } @@ -86,8 +77,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 = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice ").generate().getBytes(); - byte[] revocation = assumeSupported(sop::revokeKey).withKeyPassword(wrongPassword).withKeyPassword(password).keys(secretKey).getBytes(); + 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)); } @@ -96,9 +87,9 @@ public class RevokeKeyTest extends AbstractSOPTest { @MethodSource("provideInstances") public void revokeProtectedKeyWithMissingPassphraseFails(SOP sop) throws IOException { byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); - byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice ").generate().getBytes(); + byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice ").generate().getBytes(); - assertThrows(SOPGPException.KeyIsProtected.class, () -> assumeSupported(sop::revokeKey).keys(secretKey).getBytes()); + assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.revokeKey().keys(secretKey).getBytes()); } @ParameterizedTest @@ -106,27 +97,27 @@ public class RevokeKeyTest extends AbstractSOPTest { public void revokeProtectedKeyWithWrongPassphraseFails(SOP sop) throws IOException { byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); String wrongPassword = "or4ng3"; - byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice ").generate().getBytes(); + byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice ").generate().getBytes(); - assertThrows(SOPGPException.KeyIsProtected.class, () -> assumeSupported(sop::revokeKey).withKeyPassword(wrongPassword).keys(secretKey).getBytes()); + assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.revokeKey().withKeyPassword(wrongPassword).keys(secretKey).getBytes()); } @ParameterizedTest @MethodSource("provideInstances") public void revokeKeyIsNowHardRevoked(SOP sop) throws IOException { - byte[] key = assumeSupported(sop::generateKey).generate().getBytes(); - byte[] cert = assumeSupported(sop::extractCert).key(key).getBytes(); + 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 = assumeSupported(sop::inlineSign).key(key).data(msg).getBytes(); + byte[] signedMsg = sop.inlineSign().key(key).data(msg).getBytes(); // Verifying the message with the valid cert works - List result = assumeSupported(sop::inlineVerify).cert(cert).data(signedMsg).toByteArrayAndResult().getResult(); + 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 = assumeSupported(sop::revokeKey).keys(key).getBytes(); - assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::inlineVerify).cert(revokedCert).data(signedMsg).toByteArrayAndResult()); + byte[] revokedCert = sop.revokeKey().keys(key).getBytes(); + assertThrows(SOPGPException.NoSignature.class, () -> sop.inlineVerify().cert(revokedCert).data(signedMsg).toByteArrayAndResult()); } } diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java similarity index 52% rename from sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java rename to sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java index 71f7efd..73ba571 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java @@ -8,15 +8,14 @@ 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 sop.exception.SOPGPException; 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 { @@ -28,7 +27,7 @@ public class VersionTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void versionNameTest(SOP sop) { - String name = assumeSupported(sop::version).getName(); + String name = sop.version().getName(); assertNotNull(name); assertFalse(name.isEmpty()); } @@ -36,21 +35,21 @@ public class VersionTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void versionVersionTest(SOP sop) { - String version = assumeSupported(sop::version).getVersion(); + String version = sop.version().getVersion(); assertFalse(version.isEmpty()); } @ParameterizedTest @MethodSource("provideInstances") public void backendVersionTest(SOP sop) { - String backend = assumeSupported(sop::version).getBackendVersion(); + String backend = sop.version().getBackendVersion(); assertFalse(backend.isEmpty()); } @ParameterizedTest @MethodSource("provideInstances") public void extendedVersionTest(SOP sop) { - String extended = assumeSupported(sop::version).getExtendedVersion(); + String extended = sop.version().getExtendedVersion(); assertFalse(extended.isEmpty()); } @@ -58,38 +57,19 @@ public class VersionTest extends AbstractSOPTest { @MethodSource("provideInstances") public void sopSpecVersionTest(SOP sop) { try { - assumeSupported(sop::version).getSopSpecVersion(); + sop.version().getSopSpecVersion(); } catch (RuntimeException e) { - throw new TestAbortedException("SOP backend does not support 'version --sop-spec' yet."); + assumeTrue(false); // SOP backend does not support this operation yet } - String sopSpec = assumeSupported(sop::version).getSopSpecVersion(); - if (assumeSupported(sop::version).isSopSpecImplementationIncomplete()) { + String sopSpec = sop.version().getSopSpecVersion(); + if (sop.version().isSopSpecImplementationIncomplete()) { assertTrue(sopSpec.startsWith("~draft-dkg-openpgp-stateless-cli-")); } else { assertTrue(sopSpec.startsWith("draft-dkg-openpgp-stateless-cli-")); } - int sopRevision = assumeSupported(sop::version).getSopSpecRevisionNumber(); - assertTrue(assumeSupported(sop::version).getSopSpecRevisionName().endsWith("" + sopRevision)); - } - - @ParameterizedTest - @MethodSource("provideInstances") - public void sopVVersionTest(SOP sop) { - try { - assumeSupported(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."); - } - } - - @ParameterizedTest - @MethodSource("provideInstances") - public void sopJavaVersionTest(SOP sop) { - assertNotNull(assumeSupported(sop::version).getSopJavaVersion()); + int sopRevision = sop.version().getSopSpecRevisionNumber(); + assertTrue(sop.version().getSopSpecRevisionName().endsWith("" + sopRevision)); } } diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/package-info.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/package-info.java similarity index 100% rename from sop-java-testfixtures/src/main/java/sop/testsuite/operation/package-info.java rename to sop-java/src/testFixtures/java/sop/testsuite/operation/package-info.java diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/package-info.java b/sop-java/src/testFixtures/java/sop/testsuite/package-info.java similarity index 100% rename from sop-java-testfixtures/src/main/java/sop/testsuite/package-info.java rename to sop-java/src/testFixtures/java/sop/testsuite/package-info.java diff --git a/version.gradle b/version.gradle index bac96da..8e50153 100644 --- a/version.gradle +++ b/version.gradle @@ -4,13 +4,15 @@ allprojects { ext { - shortVersion = '14.0.1' - isSnapshot = true - javaSourceCompatibility = 11 + shortVersion = '7.0.2' + isSnapshot = false + minAndroidSdk = 10 + javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' jsrVersion = '3.0.2' junitVersion = '5.8.2' - logbackVersion = '1.5.13' + junitSysExitVersion = '1.1.2' + logbackVersion = '1.2.11' mockitoVersion = '4.5.1' picocliVersion = '4.6.3' slf4jVersion = '1.7.36'