diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..4f92d0e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** + + +**Version** + +- `sop-java`: +- `pgpainless-core`: +- `bouncycastle`: + +**To Reproduce** + +``` +Example Code Block +``` + +**Expected behavior** + + +**Additional context** + diff --git a/.reuse/dep5 b/.reuse/dep5 deleted file mode 100644 index b8bb6be..0000000 --- a/.reuse/dep5 +++ /dev/null @@ -1,15 +0,0 @@ -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: SOP-Java -Upstream-Contact: Paul Schaub -Source: https://pgpainless.org - -# Sample paragraph, commented out: -# -# Files: src/* -# Copyright: $YEAR $NAME <$CONTACT> -# License: ... - -# Gradle build tool -Files: gradle* -Copyright: 2015 the original author or authors. -License: Apache-2.0 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9deccee..0000000 --- a/.travis.yml +++ /dev/null @@ -1,52 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Paul Schaub -# -# SPDX-License-Identifier: Apache-2.0 - -language: java -dist: bionic -jdk: - - openjdk8 - - openjdk11 - -services: - - docker - -before_cache: - - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ -cache: - directories: - - $HOME/.gradle/caches/ - - $HOME/.m2 - -before_install: - - export GRADLE_VERSION=6.2 - - wget https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-all.zip - - unzip -q gradle-${GRADLE_VERSION}-all.zip - - rm gradle-${GRADLE_VERSION}-all.zip - - sudo mv gradle-${GRADLE_VERSION} /usr/local/bin/ - - export PATH="/usr/local/bin/gradle-${GRADLE_VERSION}/bin:$PATH" - - docker pull fsfe/reuse:latest - - docker run -v ${TRAVIS_BUILD_DIR}:/data fsfe/reuse:latest lint - -install: gradle assemble --stacktrace - -# Run the test suite and also install the artifacts in the local maven -# archive to additionaly test if artifact creation is -# functional. Which hasn't always be the case in the past, see -# 90cbcaebc7a89f4f771f733a33ac9f389df85be2 -# Also run javadocAll to ensure it works. -script: - - | - JAVAC_MAJOR_VERSION=$(javac -version | sed -E 's/javac ([[:digit:]]+).*/\1/') - GRADLE_TASKS=() - GRADLE_TASKS+=(check) - if [[ ${JAVAC_MAJOR_VERSION} -ge 11 ]]; then - GRADLE_TASKS+=(javadocAll) - fi - gradle ${GRADLE_TASKS[@]} --stacktrace - -after_success: - - JAVAC_VERSION=$((javac -version) 2>&1) - # Only run jacocoRootReport in the Java 8 build - - if [[ "$JAVAC_VERSION" = javac\ 1.8.* ]]; then gradle jacocoRootReport coveralls; fi diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml new file mode 100644 index 0000000..4c23ffb --- /dev/null +++ b/.woodpecker/build.yml @@ -0,0 +1,21 @@ +steps: + run: + when: + event: push + image: gradle:7.6-jdk11-jammy + commands: + # Install Sequoia-SOP + - apt update && apt install --yes sqop + # Checkout code + - git checkout $CI_COMMIT_BRANCH + # Prepare CI + - cp external-sop/src/main/resources/sop/testsuite/external/config.json.ci external-sop/src/main/resources/sop/testsuite/external/config.json + # Code works + - gradle test + # Code is clean + - gradle check javadocAll + # Code has coverage + - gradle jacocoRootReport coveralls + environment: + COVERALLS_REPO_TOKEN: + from_secret: coveralls_repo_token diff --git a/.woodpecker/reuse.yml b/.woodpecker/reuse.yml new file mode 100644 index 0000000..b278a39 --- /dev/null +++ b/.woodpecker/reuse.yml @@ -0,0 +1,9 @@ +# Code is licensed properly +# See https://reuse.software/ +steps: + reuse: + when: + event: push + image: fsfe/reuse:latest + commands: + - reuse lint diff --git a/CHANGELOG.md b/CHANGELOG.md index d60f114..0447a9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,187 @@ 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 +- CLI `change-key-password`: Fix indirect parameter passing for new and old passwords (thanks to @dkg for the report) +- Backport: revoke-key command: Allow for multiple '--with-key-password' options + +## 7.0.1 +- `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty. + +## 7.0.0 +- Update implementation to [SOP Specification revision 07](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-07.html). + - Add support for new `revoke-key` subcommand + - Add support for new `change-key-password` subcommand + - Add support for new `--signing-only` option of `generate-key` subcommand +- Add `dearmor.data(String)` utility method +- Fix typos in, and improve i18n of CLI help pages + +## 6.1.0 +- `listProfiles()`: Add shortcut methods `generateKey()` and `encrypt()` +- Add DSL for testing `Verification` results +- `Verification` + - Return `Optional` for `getSignatureMode()` + - Return `Optional` for `getDescription()` +- `Profile` + - Add support for profiles without description + - Return `Optional` for `getDescription()` + - Add `parse(String)` method for parsing profile lines +- `sop-java`: Add dependency on `com.google.code.findbugs:jsr305` for `@Nullable`, `@Nonnull` annotations +- `UTCUtil`: `parseUTCDate()` is now `@Nonnull` and throws a `ParseException` for invalid inputs +- `UTF8Util`: `decodeUTF8()` now throws `CharacterCodingException` instead of `SOPGPException.PasswordNotHumanReadable` +- `external-sop`: Properly map error codes to new exception types (ported from `5.0.1`): + - `UNSUPPORTED_PROFILE` + - `INCOMPATIBLE_OPTIONS` + +## 5.0.1 +- `external-sop`: Properly map error codes to new exception types: + - `UNSUPPORTED_PROFILE` + - `INCOMPATIBLE_OPTIONS` + +## 6.0.0 +- Update implementation to [SOP Specification revision 06](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-06.html). + - Add option `--profile=XYZ` to `encrypt` subcommand + - Add option `--sop-spec` to `version` subcommand + - `Version`: Add different getters for specification-related values + +## 5.0.0 +- Update implementation to [SOP Specification revision 05](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html). + - Add the concept of profiles + - Add `list-profiles` subcommand + - Add option `--profile=XYZ` to `generate-key` subcommand + - `Verification` objects can now optionally indicate the type of the signature (`mode:text` or `mode:binary`) + - `Verification` objects can now contain an optional description of the signature + - `inline-sign` now throws an error if incompatible options `--as=clearsigned` and `--no-armor` are used + +## 4.1.1 +- Restructure test suite to allow simultaneous testing of multiple backends +- Fix IOException in `sop sign` due to premature stream closing +- Allow for downstream implementations of `sop-java` to reuse the test suite + - Check out Javadoc of `sop-java/src/testFixtures/java/sop/testsuite/SOPInstanceFactory` for details + +## 4.1.0 +- Add module `external-sop` + - This module implements the `sop-java` interfaces and allows the use of an external SOP binary +- `decrypt`: Rename `--not-before`, `--not-after` to `--verify-not-before`, `--verify-not-after` +- `decrypt`: Throw `NoSignature` error if no verifiable signature found, but signature verification is requested using `--verify-with`. +- `inline-sign`: Fix parameter label of `--as=clearsigned` +- `ArmorLabel`, `EncryptAs`, `SignAs`: make `toString()` return lowercase + +## 4.0.7 +- Make i18n string for `--stacktrace` option translatable +- Make manpages generation reproducible +- `dearmor`: Transform `IOException` into `BadData` + +## 4.0.6 +- Add support for file descriptors on unix / linux systems + +## 4.0.5 +- `inline-sign`: Make possible values of `--as` option lowercase +- `inline-sign`: Rename value `cleartextsigned` of option `--as` to `clearsigned` + +## 4.0.4 +- Not found + +## 4.0.3 +- `decrypt`: Rename option `--verify-out` to `--verifications-out`, but keep `--verify-out` as alias +- Fix: `decrypt`: Flush output stream in order to prevent empty file as result of `--session-key-out` +- Fix: Properly format session key for `--session-key-out` +- Be less finicky about input session key formats + - Allow upper- and lowercase hexadecimal keys + - Allow trailing whitespace + +## 4.0.2 +- Fix: `verify`: Do not include detached signature in list of certificates +- Fix: `inline-verify`: Also include the first argument in list of certificates +- Hide stacktraces by default and add `--stacktrace` option to print them +- Properly throw `CannotDecrypt` exception when message could not be decrypted + +## 4.0.1 +- Use shared resources for i18n + - Fix strings not being resolved properly when downstream renames `sop` command + ## 4.0.0 - Switch to new versioning format to indicate implemented SOP version - Implement SOP specification version 04 diff --git a/README.md b/README.md index d94bfd1..35324c4 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,18 @@ SPDX-License-Identifier: Apache-2.0 # SOP for Java -[![Travis (.com)](https://travis-ci.com/pgpainless/sop-java.svg?branch=master)](https://travis-ci.com/pgpainless/sop-java) -[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/sop-java)](https://search.maven.org/artifact/org.pgpainless/sop-java) -[![Spec Revision: 4](https://img.shields.io/badge/Spec%20Revision-4-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/04/) -[![Coverage Status](https://coveralls.io/repos/github/pgpainless/sop-java/badge.svg?branch=master)](https://coveralls.io/github/pgpainless/sop-java?branch=master) +[![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/) +[![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) The [Stateless OpenPGP Protocol](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) specification defines a generic stateless CLI for dealing with OpenPGP messages. Its goal is to provide a minimal, yet powerful API for the most common OpenPGP related operations. +[![Packaging status](https://repology.org/badge/vertical-allrepos/sop-java.svg)](https://repology.org/project/pgpainless/versions) +[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/sop-java)](https://search.maven.org/artifact/org.pgpainless/sop-java) + ## Modules The repository contains the following modules: @@ -23,16 +25,23 @@ 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!) -| Project | Description | -|---------------------------------------------------------------------------------------|-----------------------------------------------| -| [pgpainless-sop](https://github.com/pgpainless/pgpainless/tree/master/pgpainless-sop) | Implementation of `sop-java` using PGPainless | +| Project | Description | +|-------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------| +| [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 | +| 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 | diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..7e1b250 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,32 @@ +# 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 56730fd..10f2b87 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,8 @@ buildscript { } plugins { - id 'ru.vyarus.animalsniffer' version '1.5.3' + id 'org.jetbrains.kotlin.jvm' version "1.9.21" + id 'com.diffplug.spotless' version '6.22.0' apply false } apply from: 'version.gradle' @@ -29,18 +30,9 @@ allprojects { apply plugin: 'eclipse' apply plugin: 'jacoco' apply plugin: 'checkstyle' - - // 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] - } - } + apply plugin: 'kotlin' + apply plugin: 'kotlin-kapt' + apply plugin: 'com.diffplug.spotless' // Only generate jar for submodules // https://stackoverflow.com/a/25445035 @@ -53,12 +45,16 @@ allprojects { toolVersion = '8.18' } + spotless { + kotlin { + ktfmt().dropboxStyle() + } + } + group 'org.pgpainless' description = "Stateless OpenPGP Protocol API for Java" version = shortVersion - sourceCompatibility = javaSourceCompatibility - repositories { mavenCentral() } @@ -67,6 +63,20 @@ 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 { @@ -94,7 +104,7 @@ allprojects { } jacoco { - toolVersion = "0.8.7" + toolVersion = "0.8.8" } jacocoTestReport { @@ -102,7 +112,7 @@ allprojects { sourceDirectories.setFrom(project.files(sourceSets.main.allSource.srcDirs)) classDirectories.setFrom(project.files(sourceSets.main.output)) reports { - xml.enabled true + xml.required = true } } @@ -120,15 +130,15 @@ subprojects { apply plugin: 'signing' task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' + archiveClassifier = 'sources' from sourceSets.main.allSource } task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' + archiveClassifier = 'javadoc' from javadoc.destinationDir } task testsJar(type: Jar, dependsOn: testClasses) { - classifier = 'tests' + archiveClassifier = 'tests' from sourceSets.test.output } @@ -225,7 +235,7 @@ task jacocoRootReport(type: JacocoReport) { classDirectories.setFrom(files(subprojects.sourceSets.main.output)) executionData.setFrom(files(subprojects.jacocoTestReport.executionData)) reports { - xml.enabled true + xml.required = true xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml") } // We could remove the following setOnlyIf line, but then @@ -236,10 +246,6 @@ task jacocoRootReport(type: JacocoReport) { } task javadocAll(type: Javadoc) { - def currentJavaVersion = JavaVersion.current() - if (currentJavaVersion.compareTo(JavaVersion.VERSION_1_9) >= 0) { - options.addStringOption("-release", "8"); - } source subprojects.collect {project -> project.sourceSets.main.allJava } destinationDir = new File(buildDir, 'javadoc') diff --git a/external-sop/README.md b/external-sop/README.md new file mode 100644 index 0000000..e54e548 --- /dev/null +++ b/external-sop/README.md @@ -0,0 +1,59 @@ + + +# External-SOP + +Access an external SOP binary from within your Java/Kotlin application. + +This module implements a backend for `sop-java` that binds to external SOP binaries (such as +[sqop](https://gitlab.com/sequoia-pgp/sequoia-sop/), [python-sop](https://pypi.org/project/sop/) etc.). +SOP operation calls will be delegated to the external binary, and the results are parsed back, so that you can +access them from your Java application as usual. + +## Example +Let's say you are using `ExampleSOP` which is a binary installed in `/usr/bin/example-sop`. +Instantiating a `SOP` object is as simple as this: + +```java +SOP sop = new ExternalSOP("/usr/bin/example-sop"); +``` + +This SOP object can now be used as usual (see [here](../sop-java/README.md)). + +Keep in mind the license of the external SOP binary when integrating one with your project! + +Some SOP binaries might require additional configuration, e.g. a Java based SOP might need to know which JAVA_HOME to use. +For this purpose, additional environment variables can be passed in using a `Properties` object: + +```java +Properties properties = new Properties(); +properties.put("JAVA_HOME", "/usr/lib/jvm/[...]"); +SOP sop = new ExternalSOP("/usr/bin/example-sop", properties); +``` + +Most results of SOP operations are communicated via standard-out, standard-in. However, some operations rely on +writing results to additional output files. +To handle such results, we need to provide a temporary directory, to which those results can be written by the SOP, +and from which `External-SOP` reads them back. +The default implementation relies on `Files.createTempDirectory()` to provide a temporary directory. +It is however possible to overwrite this behavior, in order to specify a custom, perhaps more private directory: + +```java +ExternalSOP.TempDirProvider provider = new ExternalSOP.TempDirProvider() { + @Override + public File provideTempDirectory() throws IOException { + File myTempDir = new File("/path/to/directory"); + myTempDir.mkdirs(); + return myTempDir; + } +}; +SOP sop = new ExternalSOP("/usr/bin/example-sop", provider); +``` + +## Testing +The `external-sop` module comes with a growing test suite, which tests SOP binaries against the expectations of the SOP specification. +To configure one or multiple backends for use with the test suite, just provide a custom `config.json` file in `src/main/resources/sop/external`. +An example configuration file with the required file format is available as `config.json.example`. diff --git a/external-sop/build.gradle b/external-sop/build.gradle new file mode 100644 index 0000000..2dfbf7e --- /dev/null +++ b/external-sop/build.gradle @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +plugins { + id 'java-library' +} + +group 'org.pgpainless' + +repositories { + mavenCentral() +} + +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' + + api 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" + + // 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")) +} + +test { + // Inject configured external SOP instances using our custom TestSubjectFactory + environment("test.implementation", "sop.testsuite.external.ExternalSOPInstanceFactory") + + useJUnitPlatform() + + // since we test external backends which we might not control, + // we ignore test failures in this module + ignoreFailures = true +} + diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt new file mode 100644 index 0000000..48a5af9 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -0,0 +1,344 @@ +// 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 new file mode 100644 index 0000000..3341055 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt @@ -0,0 +1,57 @@ +// 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 new file mode 100644 index 0000000..b202746 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt @@ -0,0 +1,23 @@ +// 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 new file mode 100644 index 0000000..e3661db --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.* +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.CertifyUserId + +class CertifyUserIdExternal(binary: String, environment: Properties) : CertifyUserId { + + private val commandList = mutableListOf(binary, "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 new file mode 100644 index 0000000..a45e59f --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ChangeKeyPasswordExternal.kt @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.ChangeKeyPassword + +/** Implementation of the [ChangeKeyPassword] operation using an external SOP binary. */ +class ChangeKeyPasswordExternal(binary: String, environment: Properties) : ChangeKeyPassword { + + private val commandList: MutableList = mutableListOf(binary, "change-key-password") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var keyPasswordCounter = 0 + + override fun noArmor(): ChangeKeyPassword = apply { commandList.add("--no-armor") } + + override fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword = apply { + commandList.add("--old-key-password=@ENV:KEY_PASSWORD_$keyPasswordCounter") + envList.add("KEY_PASSWORD_$keyPasswordCounter=$oldPassphrase") + keyPasswordCounter += 1 + } + + override fun newKeyPassphrase(newPassphrase: String): ChangeKeyPassword = apply { + commandList.add("--new-key-password=@ENV:KEY_PASSWORD_$keyPasswordCounter") + envList.add("KEY_PASSWORD_$keyPasswordCounter=$newPassphrase") + keyPasswordCounter += 1 + } + + override fun keys(keys: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys) +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt new file mode 100644 index 0000000..928d9b4 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.Dearmor + +/** Implementation of the [Dearmor] operation using an external SOP binary. */ +class DearmorExternal(binary: String, environment: Properties) : Dearmor { + private val commandList = listOf(binary, "dearmor") + private val envList = ExternalSOP.propertiesToEnv(environment) + + override fun data(data: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt new file mode 100644 index 0000000..1e6d6a2 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.* +import java.util.* +import sop.DecryptionResult +import sop.ReadyWithResult +import sop.SessionKey +import sop.Verification +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.external.ExternalSOP.Companion.readString +import sop.operation.Decrypt +import sop.util.UTCUtil + +/** Implementation of the [Decrypt] operation using an external SOP binary. */ +class DecryptExternal( + binary: String, + environment: Properties, + private val tempDirProvider: ExternalSOP.TempDirProvider +) : Decrypt { + + private val commandList = mutableListOf(binary, "decrypt") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + private var requireVerification = false + + override fun verifyNotBefore(timestamp: Date): Decrypt = apply { + commandList.add("--verify-not-before=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun verifyNotAfter(timestamp: Date): Decrypt = apply { + commandList.add("--verify-not-after=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun verifyWithCert(cert: InputStream): Decrypt = apply { + commandList.add("--verify-with=@ENV:VERIFY_WITH_$argCounter") + envList.add("VERIFY_WITH_$argCounter=${readString(cert)}") + argCounter += 1 + requireVerification = true + } + + override fun withSessionKey(sessionKey: SessionKey): Decrypt = apply { + commandList.add("--with-session-key=@ENV:SESSION_KEY_$argCounter") + envList.add("SESSION_KEY_$argCounter=$sessionKey") + argCounter += 1 + } + + override fun withPassword(password: String): Decrypt = apply { + commandList.add("--with-password=@ENV:PASSWORD_$argCounter") + envList.add("PASSWORD_$argCounter=$password") + argCounter += 1 + } + + override fun withKey(key: InputStream): Decrypt = apply { + commandList.add("@ENV:KEY_$argCounter") + envList.add("KEY_$argCounter=${readString(key)}") + argCounter += 1 + } + + override fun withKeyPassword(password: ByteArray): Decrypt = apply { + commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCounter") + envList.add("KEY_PASSWORD_$argCounter=${String(password)}") + argCounter += 1 + } + + override fun ciphertext(ciphertext: InputStream): ReadyWithResult { + val tempDir = tempDirProvider.provideTempDirectory() + + val sessionKeyOut = File(tempDir, "session-key-out") + sessionKeyOut.delete() + commandList.add("--session-key-out=${sessionKeyOut.absolutePath}") + + val verifyOut = File(tempDir, "verifications-out") + verifyOut.delete() + if (requireVerification) { + commandList.add("--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 new file mode 100644 index 0000000..66d1db8 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/DetachedSignExternal.kt @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.* +import java.util.* +import sop.MicAlg +import sop.ReadyWithResult +import sop.SigningResult +import sop.SigningResult.Companion.builder +import sop.enums.SignAs +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.operation.DetachedSign + +/** Implementation of the [DetachedSign] operation using an external SOP binary. */ +class DetachedSignExternal( + binary: String, + environment: Properties, + private val tempDirProvider: ExternalSOP.TempDirProvider +) : DetachedSign { + + private val commandList = mutableListOf(binary, "sign") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + + override fun mode(mode: SignAs): DetachedSign = apply { commandList.add("--as=$mode") } + + override fun data(data: InputStream): ReadyWithResult { + val tempDir = tempDirProvider.provideTempDirectory() + val micAlgOut = File(tempDir, "micAlgOut") + micAlgOut.delete() + commandList.add("--micalg-out=${micAlgOut.absolutePath}") + + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): SigningResult { + val buf = ByteArray(4096) + var r: Int + while (data.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + data.close() + try { + processOut.close() + } catch (e: IOException) { + // Ignore Stream closed + if ("Stream closed" != e.message) { + throw e + } + } + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + + processIn.close() + outputStream.close() + + finish(process) + + val builder = builder() + if (micAlgOut.exists()) { + val reader = BufferedReader(InputStreamReader(FileInputStream(micAlgOut))) + val line = reader.readLine() + if (line != null && line.isNotBlank()) { + val micAlg = MicAlg(line.trim()) + builder.setMicAlg(micAlg) + } + reader.close() + micAlgOut.delete() + } + + return builder.build() + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + override fun noArmor(): DetachedSign = apply { commandList.add("--no-armor") } + + override fun key(key: InputStream): DetachedSign = apply { + commandList.add("@ENV:KEY_$argCounter") + envList.add("KEY_$argCounter=${ExternalSOP.readString(key)}") + argCounter += 1 + } + + override fun withKeyPassword(password: ByteArray): DetachedSign = apply { + commandList.add("--with-key-password=@ENV:WITH_KEY_PASSWORD_$argCounter") + envList.add("WITH_KEY_PASSWORD_$argCounter=${String(password)}") + argCounter += 1 + } +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt new file mode 100644 index 0000000..3340a33 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.util.* +import sop.Verification +import sop.Verification.Companion.fromString +import sop.exception.SOPGPException +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.operation.DetachedVerify +import sop.operation.VerifySignatures +import sop.util.UTCUtil + +/** Implementation of the [DetachedVerify] operation using an external SOP binary. */ +class DetachedVerifyExternal(binary: String, environment: Properties) : DetachedVerify { + + private val commandList = mutableListOf(binary, "verify") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var signatures: InputStream? = null + private val certs: MutableSet = mutableSetOf() + private var argCounter = 0 + + override fun signatures(signatures: InputStream): VerifySignatures = apply { + this.signatures = signatures + } + + override fun notBefore(timestamp: Date): DetachedVerify = apply { + commandList.add("--not-before=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun notAfter(timestamp: Date): DetachedVerify = apply { + commandList.add("--not-after=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun cert(cert: InputStream): DetachedVerify = apply { this.certs.add(cert) } + + override fun data(data: InputStream): List { + // Signature + if (signatures == null) { + throw SOPGPException.MissingArg("Missing argument: signatures cannot be null.") + } + commandList.add("@ENV:SIGNATURE") + envList.add("SIGNATURE=${ExternalSOP.readString(signatures!!)}") + + // Certs + for (cert in certs) { + commandList.add("@ENV:CERT_$argCounter") + envList.add("CERT_$argCounter=${ExternalSOP.readString(cert)}") + argCounter += 1 + } + + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + val buf = ByteArray(4096) + var r: Int + while (data.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + data.close() + processOut.close() + + val bufferedReader = BufferedReader(InputStreamReader(processIn)) + val verifications: MutableList = ArrayList() + + var line: String? + while (bufferedReader.readLine().also { line = it } != null) { + verifications.add(fromString(line!!)) + } + + finish(process) + + return verifications + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt new file mode 100644 index 0000000..679e09b --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.* +import sop.EncryptionResult +import sop.ReadyWithResult +import sop.SessionKey.Companion.fromString +import sop.enums.EncryptAs +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.external.ExternalSOP.Companion.readString +import sop.operation.Encrypt + +/** Implementation of the [Encrypt] operation using an external SOP binary. */ +class EncryptExternal( + binary: String, + environment: Properties, + private val tempDirProvider: ExternalSOP.TempDirProvider +) : Encrypt { + + private val commandList = mutableListOf(binary, "encrypt") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + + override fun noArmor(): Encrypt = apply { commandList.add("--no-armor") } + + override fun mode(mode: EncryptAs): Encrypt = apply { commandList.add("--as=$mode") } + + override fun signWith(key: InputStream): Encrypt = apply { + commandList.add("--sign-with=@ENV:SIGN_WITH_$argCounter") + envList.add("SIGN_WITH_$argCounter=${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 new file mode 100644 index 0000000..9b86733 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ExtractCertExternal.kt @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.ExtractCert + +/** Implementation of the [ExtractCert] operation using an external SOP binary. */ +class ExtractCertExternal(binary: String, environment: Properties) : ExtractCert { + + private val commandList = mutableListOf(binary, "extract-cert") + private val envList = ExternalSOP.propertiesToEnv(environment) + + override fun noArmor(): ExtractCert = apply { commandList.add("--no-armor") } + + override fun key(keyInputStream: InputStream): Ready = + ExternalSOP.executeTransformingOperation( + Runtime.getRuntime(), commandList, envList, keyInputStream) +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt new file mode 100644 index 0000000..37116a4 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.util.Properties +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.GenerateKey + +/** Implementation of the [GenerateKey] operation using an external SOP binary. */ +class GenerateKeyExternal(binary: String, environment: Properties) : GenerateKey { + + private val commandList = mutableListOf(binary, "generate-key") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + + override fun noArmor(): GenerateKey = apply { commandList.add("--no-armor") } + + override fun userId(userId: String): GenerateKey = apply { commandList.add(userId) } + + override fun withKeyPassword(password: String): GenerateKey = apply { + commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCounter") + envList.add("KEY_PASSWORD_$argCounter=$password") + argCounter += 1 + } + + override fun profile(profile: String): GenerateKey = apply { + commandList.add("--profile=$profile") + } + + override fun signingOnly(): GenerateKey = apply { commandList.add("--signing-only") } + + override fun generate(): Ready = + ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList) +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt new file mode 100644 index 0000000..f44e6bb --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.* +import java.util.* +import sop.ReadyWithResult +import sop.Signatures +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.operation.InlineDetach + +/** Implementation of the [InlineDetach] operation using an external SOP binary. */ +class InlineDetachExternal( + binary: String, + environment: Properties, + private val tempDirProvider: ExternalSOP.TempDirProvider +) : InlineDetach { + + private val commandList = mutableListOf(binary, "inline-detach") + private val envList = ExternalSOP.propertiesToEnv(environment) + + override fun noArmor(): InlineDetach = apply { commandList.add("--no-armor") } + + override fun message(messageInputStream: InputStream): ReadyWithResult { + val tempDir = tempDirProvider.provideTempDirectory() + + val signaturesOut = File(tempDir, "signatures") + signaturesOut.delete() + commandList.add("--signatures-out=${signaturesOut.absolutePath}") + + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): Signatures { + val buf = ByteArray(4096) + var r: Int + while (messageInputStream.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + messageInputStream.close() + processOut.close() + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + + processIn.close() + outputStream.close() + + finish(process) + + val signaturesOutIn = FileInputStream(signaturesOut) + val signaturesBuffer = ByteArrayOutputStream() + while (signaturesOutIn.read(buf).also { r = it } > 0) { + signaturesBuffer.write(buf, 0, r) + } + signaturesOutIn.close() + signaturesOut.delete() + + val sigBytes = signaturesBuffer.toByteArray() + + return object : Signatures() { + @Throws(IOException::class) + override fun writeTo(outputStream: OutputStream) { + outputStream.write(sigBytes) + } + } + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt new file mode 100644 index 0000000..a304e85 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.enums.InlineSignAs +import sop.external.ExternalSOP +import sop.operation.InlineSign + +/** Implementation of the [InlineSign] operation using an external SOP binary. */ +class InlineSignExternal(binary: String, environment: Properties) : InlineSign { + + private val commandList = mutableListOf(binary, "inline-sign") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + + override fun mode(mode: InlineSignAs): InlineSign = apply { commandList.add("--as=$mode") } + + override fun data(data: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) + + override fun noArmor(): InlineSign = apply { commandList.add("--no-armor") } + + override fun key(key: InputStream): InlineSign = apply { + commandList.add("@ENV:KEY_$argCounter") + envList.add("KEY_$argCounter=${ExternalSOP.readString(key)}") + argCounter += 1 + } + + override fun withKeyPassword(password: ByteArray): InlineSign = apply { + commandList.add("--with-key-password=@ENV:WITH_KEY_PASSWORD_$argCounter") + envList.add("WITH_KEY_PASSWORD_$argCounter=${String(password)}") + argCounter += 1 + } +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt new file mode 100644 index 0000000..bf0c66b --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.* +import java.util.* +import sop.ReadyWithResult +import sop.Verification +import sop.Verification.Companion.fromString +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.operation.InlineVerify +import sop.util.UTCUtil + +/** Implementation of the [InlineVerify] operation using an external SOP binary. */ +class InlineVerifyExternal( + binary: String, + environment: Properties, + private val tempDirProvider: ExternalSOP.TempDirProvider +) : InlineVerify { + + private val commandList = mutableListOf(binary, "inline-verify") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + + override fun data(data: InputStream): ReadyWithResult> { + val tempDir = tempDirProvider.provideTempDirectory() + + val verificationsOut = File(tempDir, "verifications-out") + verificationsOut.delete() + commandList.add("--verifications-out=${verificationsOut.absolutePath}") + + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : ReadyWithResult>() { + override fun writeTo(outputStream: OutputStream): List { + val buf = ByteArray(4096) + var r: Int + while (data.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + data.close() + processOut.close() + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + + processIn.close() + outputStream.close() + + finish(process) + + val verificationsOutIn = FileInputStream(verificationsOut) + val reader = BufferedReader(InputStreamReader(verificationsOutIn)) + val verificationList: MutableList = mutableListOf() + var line: String? + while (reader.readLine().also { line = it } != null) { + verificationList.add(fromString(line!!.trim())) + } + + return verificationList + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + override fun notBefore(timestamp: Date): InlineVerify = apply { + commandList.add("--not-before=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun notAfter(timestamp: Date): InlineVerify = apply { + commandList.add("--not-after=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun cert(cert: InputStream): InlineVerify = apply { + commandList.add("@ENV:CERT_$argCounter") + envList.add("CERT_$argCounter=${ExternalSOP.readString(cert)}") + argCounter += 1 + } +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt new file mode 100644 index 0000000..5e8ff89 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.IOException +import java.util.Properties +import sop.Profile +import sop.external.ExternalSOP +import sop.operation.ListProfiles + +/** Implementation of the [ListProfiles] operation using an external SOP binary. */ +class ListProfilesExternal(binary: String, environment: Properties) : ListProfiles { + + private val commandList = mutableListOf(binary, "list-profiles") + private val envList = ExternalSOP.propertiesToEnv(environment) + + override fun subcommand(command: String): List { + return try { + String( + ExternalSOP.executeProducingOperation( + Runtime.getRuntime(), commandList.plus(command), envList) + .bytes) + .let { toProfiles(it) } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + companion object { + @JvmStatic + private fun toProfiles(output: String): List = + output.split("\n").filter { it.isNotBlank() }.map { Profile.parse(it) } + } +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt new file mode 100644 index 0000000..b739eb3 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.* +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.MergeCerts + +class MergeCertsExternal(binary: String, environment: Properties) : MergeCerts { + + private val commandList = mutableListOf(binary, "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 new file mode 100644 index 0000000..43795e6 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/RevokeKeyExternal.kt @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.RevokeKey + +/** Implementation of the [RevokeKey] operation using an external SOP binary. */ +class RevokeKeyExternal(binary: String, environment: Properties) : RevokeKey { + + private val commandList = mutableListOf(binary, "revoke-key") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCount = 0 + + override fun noArmor(): RevokeKey = apply { commandList.add("--no-armor") } + + override fun withKeyPassword(password: ByteArray): RevokeKey = apply { + commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount") + envList.add("KEY_PASSWORD_$argCount=${String(password)}") + argCount += 1 + } + + override fun keys(keys: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys) +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt new file mode 100644 index 0000000..b84f452 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt @@ -0,0 +1,43 @@ +// 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 new file mode 100644 index 0000000..cf4742b --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt @@ -0,0 +1,43 @@ +// 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 new file mode 100644 index 0000000..728f3b6 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt @@ -0,0 +1,102 @@ +// 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/main/resources/sop/testsuite/external/.gitignore b/external-sop/src/main/resources/sop/testsuite/external/.gitignore new file mode 100644 index 0000000..098eead --- /dev/null +++ b/external-sop/src/main/resources/sop/testsuite/external/.gitignore @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2023 Paul Schaub +# +# SPDX-License-Identifier: CC0-1.0 + +config.json \ No newline at end of file diff --git a/external-sop/src/main/resources/sop/testsuite/external/config.json.ci b/external-sop/src/main/resources/sop/testsuite/external/config.json.ci new file mode 100644 index 0000000..c7fd159 --- /dev/null +++ b/external-sop/src/main/resources/sop/testsuite/external/config.json.ci @@ -0,0 +1,8 @@ +{ + "backends": [ + { + "name": "Sequoia-SOP", + "sop": "/usr/bin/sqop" + } + ] +} \ No newline at end of file diff --git a/external-sop/src/main/resources/sop/testsuite/external/config.json.example b/external-sop/src/main/resources/sop/testsuite/external/config.json.example new file mode 100644 index 0000000..a70980b --- /dev/null +++ b/external-sop/src/main/resources/sop/testsuite/external/config.json.example @@ -0,0 +1,17 @@ +{ + "backends": [ + { + "name": "Example-SOP", + "sop": "/usr/bin/example-sop" + }, + { + "name": "Awesome-SOP", + "sop": "/usr/local/bin/awesome-sop", + "env": [ + { + "key": "myEnvironmentVariable", "value": "FooBar" + } + ] + } + ] +} \ No newline at end of file diff --git a/external-sop/src/test/java/sop/testsuite/external/ExternalSOPInstanceFactory.java b/external-sop/src/test/java/sop/testsuite/external/ExternalSOPInstanceFactory.java new file mode 100644 index 0000000..f84f7e6 --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/ExternalSOPInstanceFactory.java @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.external; + +import com.google.gson.Gson; +import sop.SOP; +import sop.external.ExternalSOP; +import sop.testsuite.SOPInstanceFactory; + +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +/** + * This implementation of {@link SOPInstanceFactory} reads the JSON file at + *
external-sop/src/main/resources/sop/testsuite/external/config.json
+ * to determine configured external test backends. + */ +public class ExternalSOPInstanceFactory extends SOPInstanceFactory { + + @Override + public Map provideSOPInstances() { + Map backends = new HashMap<>(); + TestSuite suite = readConfiguration(); + if (suite != null && !suite.backends.isEmpty()) { + for (TestSubject subject : suite.backends) { + if (!new File(subject.sop).exists()) { + continue; + } + + Properties env = new Properties(); + if (subject.env != null) { + for (Var var : subject.env) { + env.put(var.key, var.value); + } + } + + SOP sop = new ExternalSOP(subject.sop, env); + backends.put(subject.name, sop); + } + } + return backends; + } + + + public static TestSuite readConfiguration() { + Gson gson = new Gson(); + InputStream inputStream = ExternalSOPInstanceFactory.class.getResourceAsStream("config.json"); + if (inputStream == null) { + return null; + } + + InputStreamReader reader = new InputStreamReader(inputStream); + return gson.fromJson(reader, TestSuite.class); + } + + + // JSON DTOs + + public static class TestSuite { + List backends; + } + + public static class TestSubject { + String name; + String sop; + List env; + } + + public static class Var { + String key; + String value; + } +} diff --git a/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java b/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java new file mode 100644 index 0000000..aa0ae82 --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java @@ -0,0 +1,18 @@ +// 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/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 69a9715..8049c68 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 744e882..1b6c787 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MSYS* | MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/settings.gradle b/settings.gradle index cc5c0bc..84dc381 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,5 +5,8 @@ rootProject.name = 'SOP-Java' include 'sop-java', - 'sop-java-picocli' + 'sop-java-picocli', + 'sop-java-testfixtures', + 'external-sop', + 'sop-java-json-gson' diff --git a/sop-java-json-gson/README.md b/sop-java-json-gson/README.md new file mode 100644 index 0000000..9feb8ff --- /dev/null +++ b/sop-java-json-gson/README.md @@ -0,0 +1,13 @@ + + +# SOP-Java-JSON-GSON + +## JSON Parsing VERIFICATION extension JSON using Gson + +Since revision 11, the SOP specification defines VERIFICATIONS extension JSON. + +This module implements the `JSONParser` and `JSONSerializer` interfaces using Googles Gson library. diff --git a/sop-java-json-gson/build.gradle b/sop-java-json-gson/build.gradle new file mode 100644 index 0000000..4105902 --- /dev/null +++ b/sop-java-json-gson/build.gradle @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +plugins { + id 'java-library' +} + +group 'org.pgpainless' + +repositories { + mavenCentral() +} + +dependencies { + implementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + implementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" + runtimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + implementation project(":sop-java") + api "org.slf4j:slf4j-api:$slf4jVersion" + testImplementation "ch.qos.logback:logback-classic:$logbackVersion" + + // @Nonnull, @Nullable... + implementation "com.google.code.findbugs:jsr305:$jsrVersion" + + api "com.google.code.gson:gson:$gsonVersion" +} diff --git a/sop-java-json-gson/src/main/kotlin/sop/GsonParser.kt b/sop-java-json-gson/src/main/kotlin/sop/GsonParser.kt new file mode 100644 index 0000000..06adecb --- /dev/null +++ b/sop-java-json-gson/src/main/kotlin/sop/GsonParser.kt @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import com.google.gson.reflect.TypeToken +import java.text.ParseException + +class GsonParser( + private val gson: Gson = Gson() +) : Verification.JSONParser { + + override fun parse(string: String): Verification.JSON { + try { + return gson.fromJson(string, object : TypeToken(){}.type) + } catch (e: JsonSyntaxException) { + throw ParseException(e.message, 0) + } + } +} \ No newline at end of file diff --git a/sop-java-json-gson/src/main/kotlin/sop/GsonSerializer.kt b/sop-java-json-gson/src/main/kotlin/sop/GsonSerializer.kt new file mode 100644 index 0000000..410fe49 --- /dev/null +++ b/sop-java-json-gson/src/main/kotlin/sop/GsonSerializer.kt @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import com.google.gson.Gson + +class GsonSerializer( + private val gson: Gson = Gson() +) : Verification.JSONSerializer { + + override fun serialize(json: Verification.JSON): String { + return gson.toJson(json) + } +} \ No newline at end of file diff --git a/sop-java-json-gson/src/test/kotlin/sop/GsonSerializerAndParserTest.kt b/sop-java-json-gson/src/test/kotlin/sop/GsonSerializerAndParserTest.kt new file mode 100644 index 0000000..9bbef14 --- /dev/null +++ b/sop-java-json-gson/src/test/kotlin/sop/GsonSerializerAndParserTest.kt @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.text.ParseException + +class GsonSerializerAndParserTest { + + private val serializer: GsonSerializer = GsonSerializer() + private val parser: GsonParser = GsonParser() + + @Test + fun simpleSingleTest() { + val before = Verification.JSON("/tmp/alice.pgp") + + val json = serializer.serialize(before) + assertEquals("{\"signers\":[\"/tmp/alice.pgp\"]}", json) + + val after = parser.parse(json) + + assertEquals(before, after) + } + + @Test + fun simpleListTest() { + val before = Verification.JSON(listOf("/tmp/alice.pgp", "/tmp/bob.asc")) + + val json = serializer.serialize(before) + assertEquals("{\"signers\":[\"/tmp/alice.pgp\",\"/tmp/bob.asc\"]}", json) + + val after = parser.parse(json) + + assertEquals(before, after) + } + + @Test + fun withCommentTest() { + val before = Verification.JSON( + listOf("/tmp/alice.pgp"), + "This is a comment.", + null) + + val json = serializer.serialize(before) + assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\"}", json) + + val after = parser.parse(json) + + assertEquals(before, after) + } + + @Test + fun withExtStringTest() { + val before = Verification.JSON( + listOf("/tmp/alice.pgp"), + "This is a comment.", + "This is an ext object string.") + + val json = serializer.serialize(before) + assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\",\"ext\":\"This is an ext object string.\"}", json) + + val after = parser.parse(json) + + assertEquals(before, after) + } + + @Test + fun withExtListTest() { + val before = Verification.JSON( + listOf("/tmp/alice.pgp"), + "This is a comment.", + listOf(1.0,2.0,3.0)) + + val json = serializer.serialize(before) + assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\",\"ext\":[1.0,2.0,3.0]}", json) + + val after = parser.parse(json) + + assertEquals(before, after) + } + + @Test + fun parseInvalidJSON() { + assertThrows { parser.parse("Invalid") } + } + + @Test + fun parseMalformedJSON() { + // Missing '}' + assertThrows { parser.parse("{\"signers\":[\"Alice\"]") } + } +} \ No newline at end of file diff --git a/sop-java-picocli/build.gradle b/sop-java-picocli/build.gradle index e78bca9..2203abe 100644 --- a/sop-java-picocli/build.gradle +++ b/sop-java-picocli/build.gradle @@ -4,6 +4,7 @@ plugins { id 'application' + id 'org.asciidoctor.jvm.convert' version '3.3.2' } dependencies { @@ -11,18 +12,16 @@ 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" // @Nonnull, @Nullable... implementation "com.google.code.findbugs:jsr305:$jsrVersion" @@ -34,8 +33,13 @@ application { mainClass = mainClassName } +compileJava { + options.compilerArgs += ["-Aproject=${project.group}/${project.name}"] +} + jar { dependsOn(":sop-java:jar") + duplicatesStrategy(DuplicatesStrategy.EXCLUDE) manifest { attributes 'Main-Class': "$mainClassName" @@ -49,3 +53,25 @@ jar { exclude "META-INF/*.RSA" } } + +task generateManpageAsciiDoc(type: JavaExec) { + dependsOn(classes) + group = "Documentation" + description = "Generate AsciiDoc manpage" + classpath(configurations.annotationProcessor, sourceSets.main.runtimeClasspath) + systemProperty("user.language", "en") + main 'picocli.codegen.docgen.manpage.ManPageGenerator' + args mainClassName, "--outdir=${project.buildDir}/generated-picocli-docs", "-v" //, "--template-dir=src/docs/mantemplates" +} + +apply plugin: 'org.asciidoctor.jvm.convert' +asciidoctor { + attributes 'reproducible': '' + dependsOn(generateManpageAsciiDoc) + sourceDir = file("${project.buildDir}/generated-picocli-docs") + outputDir = file("${project.buildDir}/docs") + logDocuments = true + outputOptions { + backends = ['manpage', 'html5'] + } +} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java b/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java deleted file mode 100644 index d6474e1..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -public class Print { - - public static void errln(String string) { - // CHECKSTYLE:OFF - System.err.println(string); - // CHECKSTYLE:ON - } - - public static void trace(Throwable e) { - // CHECKSTYLE:OFF - e.printStackTrace(); - // CHECKSTYLE:ON - } - - 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 deleted file mode 100644 index 8b38af3..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.CommandLine; -import sop.exception.SOPGPException; - -public class SOPExceptionExitCodeMapper implements CommandLine.IExitCodeExceptionMapper { - - @Override - public int getExitCode(Throwable exception) { - if (exception instanceof SOPGPException) { - return ((SOPGPException) exception).getExitCode(); - } - if (exception instanceof CommandLine.UnmatchedArgumentException) { - CommandLine.UnmatchedArgumentException ex = (CommandLine.UnmatchedArgumentException) exception; - // Unmatched option of subcommand (eg. `generate-key -k`) - if (ex.isUnknownOption()) { - return SOPGPException.UnsupportedOption.EXIT_CODE; - } - // Unmatched subcommand - return SOPGPException.UnsupportedSubcommand.EXIT_CODE; - } - // Invalid option (eg. `--label Invalid`) - if (exception instanceof CommandLine.ParameterException) { - return SOPGPException.UnsupportedOption.EXIT_CODE; - } - - // Others, like IOException etc. - return 1; - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java deleted file mode 100644 index c0cabea..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.CommandLine; - -public class SOPExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler { - - @Override - public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) { - - int exitCode = commandLine.getExitCodeExceptionMapper() != null ? - commandLine.getExitCodeExceptionMapper().getExitCode(ex) : - commandLine.getCommandSpec().exitCodeOnExecutionException(); - - CommandLine.Help.ColorScheme colorScheme = commandLine.getColorScheme(); - // CHECKSTYLE:OFF - if (ex.getMessage() != null) { - commandLine.getErr().println(colorScheme.errorText(ex.getMessage())); - } - 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 deleted file mode 100644 index 750cfa8..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.AutoComplete; -import picocli.CommandLine; -import sop.SOP; -import sop.cli.picocli.commands.ArmorCmd; -import sop.cli.picocli.commands.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.SignCmd; -import sop.cli.picocli.commands.VerifyCmd; -import sop.cli.picocli.commands.VersionCmd; - -import java.util.List; -import java.util.Locale; -import java.util.ResourceBundle; - -@CommandLine.Command( - name = "sop", - resourceBundle = "sop", - exitCodeOnInvalidInput = 69, - subcommands = { - CommandLine.HelpCommand.class, - ArmorCmd.class, - DearmorCmd.class, - DecryptCmd.class, - InlineDetachCmd.class, - EncryptCmd.class, - ExtractCertCmd.class, - GenerateKeyCmd.class, - SignCmd.class, - VerifyCmd.class, - InlineSignCmd.class, - InlineVerifyCmd.class, - VersionCmd.class, - AutoComplete.GenerateCompletion.class - } -) -public class SopCLI { - // Singleton - static SOP SOP_INSTANCE; - static ResourceBundle cliMsg = ResourceBundle.getBundle("sop"); - - public static String EXECUTABLE_NAME = "sop"; - - 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); - - cliMsg = ResourceBundle.getBundle("sop"); - - // Prepare CLI - CommandLine cmd = new CommandLine(SopCLI.class); - // Hide generate-completion command - CommandLine gen = cmd.getSubcommands().get("generate-completion"); - gen.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 deleted file mode 100644 index ea73fc5..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java +++ /dev/null @@ -1,229 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import sop.exception.SOPGPException; -import sop.util.UTCUtil; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Collection; -import java.util.Date; -import java.util.Locale; -import java.util.ResourceBundle; - -public abstract class AbstractSopCmd implements Runnable { - - 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); - - protected final ResourceBundle messages; - protected EnvironmentVariableResolver envResolver = System::getenv; - - public AbstractSopCmd() { - this(Locale.getDefault()); - } - - public AbstractSopCmd(@Nonnull Locale locale) { - messages = ResourceBundle.getBundle("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); - } - - String errorMsg = getMsg("sop.error.indirect_data_type.designator_fd_not_supported"); - throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg); - - } 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); - } - - if (trimmed.startsWith(PRFX_FD)) { - String errorMsg = getMsg("sop.error.indirect_data_type.designator_fd_not_supported"); - throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg); - } - - 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 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) { - Date date = notAfter.equals("now") ? new Date() : notAfter.equals("-") ? END_OF_TIME : UTCUtil.parseUTCDate(notAfter); - if (date == null) { - String errorMsg = getMsg("sop.error.input.malformed_not_after"); - throw new IllegalArgumentException(errorMsg); - } - return date; - } - - public Date parseNotBefore(String notBefore) { - Date date = notBefore.equals("now") ? new Date() : notBefore.equals("-") ? BEGINNING_OF_TIME : UTCUtil.parseUTCDate(notBefore); - if (date == null) { - String errorMsg = getMsg("sop.error.input.malformed_not_before"); - throw new IllegalArgumentException(errorMsg); - } - return date; - } - -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java deleted file mode 100644 index 891b974..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; -import sop.operation.Armor; - -import java.io.IOException; - -@CommandLine.Command(name = "armor", - resourceBundle = "sop", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ArmorCmd extends AbstractSopCmd { - - @CommandLine.Option(names = {"--label"}, - descriptionKey = "sop.armor.usage.option.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/DearmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java deleted file mode 100644 index 1bca4d7..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Dearmor; - -import java.io.IOException; - -@CommandLine.Command(name = "dearmor", - resourceBundle = "sop", - 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) { - 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 deleted file mode 100644 index 5a9b4f5..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java +++ /dev/null @@ -1,256 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.Verification; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Decrypt; -import sop.util.HexUtil; - -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; -import java.util.regex.Pattern; - -@CommandLine.Command(name = "decrypt", - resourceBundle = "sop", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class DecryptCmd extends AbstractSopCmd { - - private static final String OPT_WITH_SESSION_KEY = "--with-session-key"; - private static final String OPT_WITH_PASSWORD = "--with-password"; - private static final String OPT_NOT_BEFORE = "--not-before"; - private static final String OPT_NOT_AFTER = "--not-after"; - private static final String OPT_SESSION_KEY_OUT = "--session-key-out"; - private static final String OPT_VERIFY_OUT = "--verify-out"; - private static final String OPT_VERIFY_WITH = "--verify-with"; - private static final String OPT_WITH_KEY_PASSWORD = "--with-key-password"; - - - @CommandLine.Option( - names = {OPT_SESSION_KEY_OUT}, - descriptionKey = "sop.decrypt.usage.option.session_key_out", - paramLabel = "SESSIONKEY") - String sessionKeyOut; - - @CommandLine.Option( - names = {OPT_WITH_SESSION_KEY}, - descriptionKey = "sop.decrypt.usage.option.with_session_key", - paramLabel = "SESSIONKEY") - List withSessionKey = new ArrayList<>(); - - @CommandLine.Option( - names = {OPT_WITH_PASSWORD}, - descriptionKey = "sop.decrypt.usage.option.with_password", - paramLabel = "PASSWORD") - List withPassword = new ArrayList<>(); - - @CommandLine.Option(names = {OPT_VERIFY_OUT}, - descriptionKey = "sop.decrypt.usage.option.verify_out", - paramLabel = "VERIFICATIONS") - String verifyOut; - - @CommandLine.Option(names = {OPT_VERIFY_WITH}, - descriptionKey = "sop.decrypt.usage.option.certs", - paramLabel = "CERT") - List certs = new ArrayList<>(); - - @CommandLine.Option(names = {OPT_NOT_BEFORE}, - descriptionKey = "sop.decrypt.usage.option.not_before", - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {OPT_NOT_AFTER}, - descriptionKey = "sop.decrypt.usage.option.not_after", - paramLabel = "DATE") - String notAfter = "now"; - - @CommandLine.Parameters(index = "0..*", - descriptionKey = "sop.decrypt.usage.param.keys", - paramLabel = "KEY") - List keys = new ArrayList<>(); - - @CommandLine.Option(names = {OPT_WITH_KEY_PASSWORD}, - descriptionKey = "sop.decrypt.usage.option.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_VERIFY_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 (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) { - 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)); - } else { - SessionKey sessionKey = result.getSessionKey().get(); - outputStream.write(sessionKey.getAlgorithm()); - outputStream.write(sessionKey.getKey()); - } - } - } - } - - 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) { - Pattern sessionKeyPattern = Pattern.compile("^\\d+:[0-9A-F]+$"); - for (String sessionKeyFile : withSessionKey) { - String sessionKey; - try { - sessionKey = stringFromInputStream(getInput(sessionKeyFile)); - } catch (IOException e) { - throw new RuntimeException(e); - } - if (!sessionKeyPattern.matcher(sessionKey).matches()) { - String errorMsg = getMsg("sop.error.input.malformed_session_key"); - throw new IllegalArgumentException(errorMsg); - } - String[] split = sessionKey.split(":"); - byte algorithm = (byte) Integer.parseInt(split[0]); - byte[] key = HexUtil.hexToBytes(split[1]); - - try { - decrypt.withSessionKey(new SessionKey(algorithm, key)); - } 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 deleted file mode 100644 index 3af1bb1..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java +++ /dev/null @@ -1,147 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.operation.Encrypt; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "encrypt", - resourceBundle = "sop", - exitCodeOnInvalidInput = 37) -public class EncryptCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - descriptionKey = "sop.encrypt.usage.option.armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = {"--as"}, - descriptionKey = "sop.encrypt.usage.option.type", - paramLabel = "{binary|text}") - EncryptAs type; - - @CommandLine.Option(names = "--with-password", - descriptionKey = "sop.encrypt.usage.option.with_password", - paramLabel = "PASSWORD") - List withPassword = new ArrayList<>(); - - @CommandLine.Option(names = "--sign-with", - descriptionKey = "sop.encrypt.usage.option.sign_with", - paramLabel = "KEY") - List signWith = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - descriptionKey = "sop.encrypt.usage.option.with_key_password", - paramLabel = "PASSWORD") - List withKeyPassword = new ArrayList<>(); - - @CommandLine.Parameters(descriptionKey = "sop.encrypt.usage.param.certs", - index = "0..*", - paramLabel = "CERTS") - List certs = new ArrayList<>(); - - @Override - public void run() { - Encrypt encrypt = throwIfUnsupportedSubcommand( - SopCLI.getSop().encrypt(), "encrypt"); - - 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 deleted file mode 100644 index 960a263..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.IOException; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.ExtractCert; - -@CommandLine.Command(name = "extract-cert", - resourceBundle = "sop", - exitCodeOnInvalidInput = 37) -public class ExtractCertCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - descriptionKey = "sop.extract-cert.usage.option.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 deleted file mode 100644 index fbf415c..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.GenerateKey; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "generate-key", - resourceBundle = "sop", - exitCodeOnInvalidInput = 37) -public class GenerateKeyCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - descriptionKey = "sop.generate-key.usage.option.armor", - negatable = true) - boolean armor = true; - - @CommandLine.Parameters(descriptionKey = "sop.generate-key.usage.option.user_id") - List userId = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - descriptionKey = "sop.generate-key.usage.option.with_key_password", - paramLabel = "PASSWORD") - String withKeyPassword; - - @Override - public void run() { - GenerateKey generateKey = throwIfUnsupportedSubcommand( - SopCLI.getSop().generateKey(), "generate-key"); - - 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 deleted file mode 100644 index bcd269d..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Signatures; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.InlineDetach; - -import java.io.IOException; -import java.io.OutputStream; - -@CommandLine.Command(name = "inline-detach", - resourceBundle = "sop", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class InlineDetachCmd extends AbstractSopCmd { - - @CommandLine.Option( - names = {"--signatures-out"}, - descriptionKey = "sop.inline-detach.usage.option.signatures_out", - paramLabel = "SIGNATURES") - String signaturesOut; - - @CommandLine.Option(names = "--no-armor", - descriptionKey = "sop.inline-detach.usage.option.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 deleted file mode 100644 index 2cf5ebd..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.enums.InlineSignAs; -import sop.exception.SOPGPException; -import sop.operation.InlineSign; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "inline-sign", - resourceBundle = "sop", - exitCodeOnInvalidInput = 37) -public class InlineSignCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - descriptionKey = "sop.inline-sign.usage.option.armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = "--as", - descriptionKey = "sop.inline-sign.usage.option.as", - paramLabel = "{binary|text|cleartextsigned}") - InlineSignAs type; - - @CommandLine.Parameters(descriptionKey = "sop.inline-sign.usage.parameter.keys", - paramLabel = "KEYS") - List secretKeyFile = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - descriptionKey = "sop.inline-sign.usage.option.with_key_password", - paramLabel = "PASSWORD") - List withKeyPassword = new ArrayList<>(); - - @Override - public void run() { - InlineSign inlineSign = throwIfUnsupportedSubcommand( - SopCLI.getSop().inlineSign(), "inline-sign"); - - 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 deleted file mode 100644 index 249d8a1..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.ReadyWithResult; -import sop.Verification; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.InlineVerify; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "inline-verify", - resourceBundle = "sop", - exitCodeOnInvalidInput = 37) -public class InlineVerifyCmd extends AbstractSopCmd { - - @CommandLine.Parameters(arity = "1..*", - descriptionKey = "sop.inline-verify.usage.parameter.certs", - paramLabel = "CERT") - List certificates = new ArrayList<>(); - - @CommandLine.Option(names = {"--not-before"}, - descriptionKey = "sop.inline-verify.usage.option.not_before", - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {"--not-after"}, - descriptionKey = "sop.inline-verify.usage.option.not_after", - paramLabel = "DATE") - String notAfter = "now"; - - @CommandLine.Option(names = "--verifications-out", - descriptionKey = "sop.inline-verify.usage.option.verifications_out") - 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 - } - } 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 deleted file mode 100644 index b6661af..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.MicAlg; -import sop.ReadyWithResult; -import sop.SigningResult; -import sop.cli.picocli.SopCLI; -import sop.enums.SignAs; -import sop.exception.SOPGPException; -import sop.operation.DetachedSign; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "sign", - resourceBundle = "sop", - exitCodeOnInvalidInput = 37) -public class SignCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - descriptionKey = "sop.sign.usage.option.armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = "--as", - descriptionKey = "sop.sign.usage.option.as", - paramLabel = "{binary|text}") - SignAs type; - - @CommandLine.Parameters(descriptionKey = "sop.sign.usage.parameter.keys", - paramLabel = "KEYS") - List secretKeyFile = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - descriptionKey = "sop.sign.usage.option.with_key_password", - paramLabel = "PASSWORD") - List withKeyPassword = new ArrayList<>(); - - @CommandLine.Option(names = "--micalg-out", - descriptionKey = "sop.sign.usage.option.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 deleted file mode 100644 index e0953b3..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Verification; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.DetachedVerify; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "verify", - resourceBundle = "sop", - exitCodeOnInvalidInput = 37) -public class VerifyCmd extends AbstractSopCmd { - - @CommandLine.Parameters(index = "0", - descriptionKey = "sop.verify.usage.parameter.signature", - paramLabel = "SIGNATURE") - String signature; - - @CommandLine.Parameters(index = "0..*", - arity = "1..*", - descriptionKey = "sop.verify.usage.parameter.certs", - paramLabel = "CERT") - List certificates = new ArrayList<>(); - - @CommandLine.Option(names = {"--not-before"}, - descriptionKey = "sop.verify.usage.option.not_before", - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {"--not-after"}, - descriptionKey = "sop.verify.usage.option.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 deleted file mode 100644 index 2dc08db..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.operation.Version; - -@CommandLine.Command(name = "version", resourceBundle = "sop", - exitCodeOnInvalidInput = 37) -public class VersionCmd extends AbstractSopCmd { - - @CommandLine.ArgGroup() - Exclusive exclusive; - - static class Exclusive { - @CommandLine.Option(names = "--extended", - descriptionKey = "sop.version.usage.option.extended") - boolean extended; - - @CommandLine.Option(names = "--backend", - descriptionKey = "sop.version.usage.option.backend") - boolean backend; - } - - - - @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; - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java deleted file mode 100644 index fc6aefd..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Subcommands of the PGPainless SOP. - */ -package sop.cli.picocli.commands; diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java deleted file mode 100644 index 83f426d..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Implementation of the Stateless OpenPGP Command Line Interface using Picocli. - */ -package sop.cli.picocli; diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt new file mode 100644 index 0000000..5778bb9 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt @@ -0,0 +1,33 @@ +// 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 new file mode 100644 index 0000000..52236d3 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExecutionExceptionHandler.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli + +import picocli.CommandLine +import picocli.CommandLine.IExecutionExceptionHandler + +class SOPExecutionExceptionHandler : IExecutionExceptionHandler { + override fun handleExecutionException( + ex: Exception, + commandLine: CommandLine, + parseResult: CommandLine.ParseResult + ): Int { + val exitCode = + if (commandLine.exitCodeExceptionMapper != null) + commandLine.exitCodeExceptionMapper.getExitCode(ex) + else commandLine.commandSpec.exitCodeOnExecutionException() + + val colorScheme = commandLine.colorScheme + if (ex.message != null) { + commandLine.getErr().println(colorScheme.errorText(ex.message)) + } else { + commandLine.getErr().println(ex.javaClass.getName()) + } + + if (SopCLI.stacktrace) { + ex.printStackTrace(commandLine.getErr()) + } + + return exitCode + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt new file mode 100644 index 0000000..07caa03 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -0,0 +1,121 @@ +// 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 new file mode 100644 index 0000000..311a446 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt @@ -0,0 +1,99 @@ +// 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 new file mode 100644 index 0000000..65be1be --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt @@ -0,0 +1,348 @@ +// 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 new file mode 100644 index 0000000..50716f1 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt @@ -0,0 +1,32 @@ +// 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 new file mode 100644 index 0000000..228809b --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt @@ -0,0 +1,84 @@ +// 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 new file mode 100644 index 0000000..be37309 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt @@ -0,0 +1,52 @@ +// 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 new file mode 100644 index 0000000..09d2a71 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt @@ -0,0 +1,41 @@ +// 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 new file mode 100644 index 0000000..de98f17 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt @@ -0,0 +1,224 @@ +// 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 new file mode 100644 index 0000000..856bc76 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt @@ -0,0 +1,159 @@ +// 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 new file mode 100644 index 0000000..cff996f --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ExtractCertCmd.kt @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.exception.SOPGPException.BadData + +@Command( + name = "extract-cert", + resourceBundle = "msg_extract-cert", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class ExtractCertCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + override fun run() { + val extractCert = + throwIfUnsupportedSubcommand(SopCLI.getSop().extractCert(), "extract-cert") + + if (!armor) { + extractCert.noArmor() + } + + try { + val ready = extractCert.key(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_a_private_key") + throw BadData(errorMsg, badData) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt new file mode 100644 index 0000000..7fa5a70 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.UnsupportedOption +import sop.exception.SOPGPException.UnsupportedProfile + +@Command( + name = "generate-key", + resourceBundle = "msg_generate-key", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class GenerateKeyCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Parameters(paramLabel = "USERID") var userId: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: String? = null + + @Option(names = ["--profile"], paramLabel = "PROFILE") var profile: String? = null + + @Option(names = ["--signing-only"]) var signingOnly: Boolean = false + + override fun run() { + val generateKey = + throwIfUnsupportedSubcommand(SopCLI.getSop().generateKey(), "generate-key") + + profile?.let { + try { + generateKey.profile(it) + } catch (e: UnsupportedProfile) { + val errorMsg = + getMsg("sop.error.usage.profile_not_supported", "generate-key", profile!!) + throw UnsupportedProfile(errorMsg, e) + } + } + + if (signingOnly) { + generateKey.signingOnly() + } + + for (userId in userId) { + generateKey.userId(userId) + } + + if (!armor) { + generateKey.noArmor() + } + + withKeyPassword?.let { + try { + val password = stringFromInputStream(getInput(it)) + generateKey.withKeyPassword(password) + } catch (e: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw UnsupportedOption(errorMsg, e) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + try { + val ready = generateKey.generate() + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt new file mode 100644 index 0000000..e311adf --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import java.lang.RuntimeException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException + +@Command( + name = "inline-detach", + resourceBundle = "msg_inline-detach", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class InlineDetachCmd : AbstractSopCmd() { + + @Option(names = ["--signatures-out"], paramLabel = "SIGNATURES") + var signaturesOut: String? = null + + @Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true + + override fun run() { + val inlineDetach = + throwIfUnsupportedSubcommand(SopCLI.getSop().inlineDetach(), "inline-detach") + + throwIfOutputExists(signaturesOut) + throwIfMissingArg(signaturesOut, "--signatures-out") + + if (!armor) { + inlineDetach.noArmor() + } + + try { + getOutput(signaturesOut).use { sigOut -> + inlineDetach + .message(System.`in`) + .writeTo(System.out) // message out + .writeTo(sigOut) // signatures out + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt new file mode 100644 index 0000000..c41f6f6 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.enums.InlineSignAs +import sop.exception.SOPGPException.* + +@Command( + name = "inline-sign", + resourceBundle = "msg_inline-sign", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class InlineSignCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--as"], paramLabel = "{binary|text|clearsigned}") + var type: InlineSignAs? = null + + @Parameters(paramLabel = "KEYS") var secretKeyFile: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + override fun run() { + val inlineSign = throwIfUnsupportedSubcommand(SopCLI.getSop().inlineSign(), "inline-sign") + + if (!armor && type == InlineSignAs.clearsigned) { + val errorMsg = getMsg("sop.error.usage.incompatible_options.clearsigned_no_armor") + throw IncompatibleOptions(errorMsg) + } + + type?.let { + try { + inlineSign.mode(it) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + } + + if (secretKeyFile.isEmpty()) { + val errorMsg = getMsg("sop.error.usage.parameter_required", "KEYS") + throw MissingArg(errorMsg) + } + + for (passwordFile in withKeyPassword) { + try { + val password = stringFromInputStream(getInput(passwordFile)) + inlineSign.withKeyPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + for (keyInput in secretKeyFile) { + try { + getInput(keyInput).use { keyIn -> inlineSign.key(keyIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (e: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput) + throw KeyIsProtected(errorMsg, e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput) + throw BadData(errorMsg, badData) + } + } + + if (!armor) { + inlineSign.noArmor() + } + + try { + val ready = inlineSign.data(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineVerifyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineVerifyCmd.kt new file mode 100644 index 0000000..6a641a6 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineVerifyCmd.kt @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import java.io.PrintWriter +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.* + +@Command( + name = "inline-verify", + resourceBundle = "msg_inline-verify", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class InlineVerifyCmd : AbstractSopCmd() { + + @Parameters(arity = "0..*", paramLabel = "CERT") var certificates: List = listOf() + + @Option(names = ["--not-before"], paramLabel = "DATE") var notBefore: String = "-" + + @Option(names = ["--not-after"], paramLabel = "DATE") var notAfter: String = "now" + + @Option(names = ["--verifications-out"], paramLabel = "VERIFICATIONS") + var verificationsOut: String? = null + + override fun run() { + val inlineVerify = + throwIfUnsupportedSubcommand(SopCLI.getSop().inlineVerify(), "inline-verify") + + throwIfOutputExists(verificationsOut) + + try { + inlineVerify.notAfter(parseNotAfter(notAfter)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + try { + inlineVerify.notBefore(parseNotBefore(notBefore)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + for (certInput in certificates) { + try { + getInput(certInput).use { certIn -> inlineVerify.cert(certIn) } + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (unsupportedAsymmetricAlgo: UnsupportedAsymmetricAlgo) { + val errorMsg = + getMsg( + "sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput) + throw UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput) + throw BadData(errorMsg, badData) + } + } + + val verifications = + try { + val ready = inlineVerify.data(System.`in`) + ready.writeTo(System.out) + } catch (e: NoSignature) { + val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found") + throw NoSignature(errorMsg, e) + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_a_message") + throw BadData(errorMsg, badData) + } + + verificationsOut?.let { + try { + getOutput(it).use { outputStream -> + val pw = PrintWriter(outputStream) + for (verification in verifications) { + pw.println(verification) + } + pw.flush() + pw.close() + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt new file mode 100644 index 0000000..b770e82 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import picocli.CommandLine.Command +import picocli.CommandLine.Parameters +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.exception.SOPGPException.UnsupportedProfile + +@Command( + name = "list-profiles", + resourceBundle = "msg_list-profiles", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class ListProfilesCmd : AbstractSopCmd() { + + @Parameters(paramLabel = "COMMAND", arity = "1", descriptionKey = "subcommand") + lateinit var subcommand: String + + override fun run() { + val listProfiles = + throwIfUnsupportedSubcommand(SopCLI.getSop().listProfiles(), "list-profiles") + + try { + listProfiles.subcommand(subcommand).forEach { println(it) } + } catch (e: UnsupportedProfile) { + val errorMsg = + getMsg("sop.error.feature_support.subcommand_does_not_support_profiles", subcommand) + throw UnsupportedProfile(errorMsg, e) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt new file mode 100644 index 0000000..3dcef38 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt @@ -0,0 +1,46 @@ +// 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 new file mode 100644 index 0000000..b9b1015 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.exception.SOPGPException.KeyIsProtected + +@Command( + name = "revoke-key", + resourceBundle = "msg_revoke-key", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class RevokeKeyCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD", 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 new file mode 100644 index 0000000..6860477 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/SignCmd.kt @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.enums.SignAs +import sop.exception.SOPGPException +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.KeyIsProtected + +@Command( + name = "sign", + resourceBundle = "msg_detached-sign", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class SignCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true + + @Option(names = ["--as"], paramLabel = "{binary|text}") var type: SignAs? = null + + @Parameters(paramLabel = "KEYS") var secretKeyFile: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + @Option(names = ["--micalg-out"], paramLabel = "MICALG") var micAlgOut: String? = null + + override fun run() { + val detachedSign = throwIfUnsupportedSubcommand(SopCLI.getSop().detachedSign(), "sign") + + throwIfOutputExists(micAlgOut) + throwIfEmptyParameters(secretKeyFile, "KEYS") + + try { + type?.let { detachedSign.mode(it) } + } catch (unsupported: SOPGPException.UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw SOPGPException.UnsupportedOption(errorMsg, unsupported) + } catch (ioe: IOException) { + throw RuntimeException(ioe) + } + + withKeyPassword.forEach { passIn -> + try { + val password = stringFromInputStream(getInput(passIn)) + detachedSign.withKeyPassword(password) + } catch (unsupported: SOPGPException.UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw SOPGPException.UnsupportedOption(errorMsg, unsupported) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + secretKeyFile.forEach { keyIn -> + try { + getInput(keyIn).use { input -> detachedSign.key(input) } + } catch (ioe: IOException) { + throw RuntimeException(ioe) + } catch (keyIsProtected: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyIn) + throw KeyIsProtected(errorMsg, keyIsProtected) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", keyIn) + throw BadData(errorMsg, badData) + } + } + + if (!armor) { + detachedSign.noArmor() + } + + try { + val ready = detachedSign.data(System.`in`) + val result = ready.writeTo(System.out) + + if (micAlgOut != null) { + getOutput(micAlgOut).use { result.micAlg.writeTo(it) } + } + } catch (e: IOException) { + throw java.lang.RuntimeException(e) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt new file mode 100644 index 0000000..931f241 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt @@ -0,0 +1,79 @@ +// 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 new file mode 100644 index 0000000..b83e5a8 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt @@ -0,0 +1,82 @@ +// 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 new file mode 100644 index 0000000..ef27266 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VerifyCmd.kt @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.* + +@Command( + name = "verify", + resourceBundle = "msg_detached-verify", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class VerifyCmd : AbstractSopCmd() { + + @Parameters(index = "0", paramLabel = "SIGNATURE") lateinit var signature: String + + @Parameters(index = "1..*", arity = "1..*", paramLabel = "CERT") + lateinit var certificates: List + + @Option(names = ["--not-before"], paramLabel = "DATE") var notBefore: String = "-" + + @Option(names = ["--not-after"], paramLabel = "DATE") var notAfter: String = "now" + + override fun run() { + val detachedVerify = + throwIfUnsupportedSubcommand(SopCLI.getSop().detachedVerify(), "verify") + try { + detachedVerify.notAfter(parseNotAfter(notAfter)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + try { + detachedVerify.notBefore(parseNotBefore(notBefore)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + for (certInput in certificates) { + try { + getInput(certInput).use { certIn -> detachedVerify.cert(certIn) } + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput) + throw BadData(errorMsg, badData) + } + } + + try { + getInput(signature).use { sigIn -> detachedVerify.signatures(sigIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_signature", signature) + throw BadData(errorMsg, badData) + } + + val verifications = + try { + detachedVerify.data(System.`in`) + } catch (e: NoSignature) { + val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found") + throw NoSignature(errorMsg, e) + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_a_message") + throw BadData(errorMsg, badData) + } + + for (verification in verifications) { + println(verification.toString()) + } + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt new file mode 100644 index 0000000..8b1936a --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt @@ -0,0 +1,57 @@ +// 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 new file mode 100644 index 0000000..1b7c1fb --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_armor.properties @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Add ASCII Armor to standard input + +standardInput=BINARY +standardInputDescription=OpenPGP material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED) +standardOutput=ARMORED +standardOutputDescription=Same material, but with ASCII-armoring added, if not already present + +stacktrace=Print stacktrace +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.synopsisHeading=Usage:\u0020 +usage.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 new file mode 100644 index 0000000..34383c8 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_armor_de.properties @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Schütze Standard-Eingabe mit ASCII Armor + +standardInputDescription=OpenPGP Material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED) +standardOutputDescription=Dasselbe Material, aber mit ASCII Armor kodiert, falls noch nicht geschehen + +stacktrace=Stacktrace ausgeben +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.synopsisHeading=Aufruf:\u0020 +usage.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 new file mode 100644 index 0000000..36dc6f4 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_certify-userid.properties @@ -0,0 +1,25 @@ +# 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 new file mode 100644 index 0000000..d634c59 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties @@ -0,0 +1,22 @@ +# 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 new file mode 100644 index 0000000..79bc11b --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_change-key-password.properties @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2023 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Update the password of a key +usage.description.0=Unlock all secret keys from STDIN using the given old passwords and emit them re-locked using the new password to STDOUT. +usage.description.1=If any (sub-) key cannot be unlocked, this operation will exit with error code 67. +no-armor=ASCII armor the output +new-key-password.0=New password to lock the keys with. +new-key-password.1=If no new password is passed in, the keys will be emitted unlocked. +new-key-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +old-key-password.0=Old passwords to unlock the keys with. +old-key-password.1=Multiple passwords can be passed in, which are tested sequentially to unlock locked subkeys. +old-key-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). + +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.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties b/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties new file mode 100644 index 0000000..5515c1d --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2023 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Ändere das Passwort eines Schlüssels +usage.description.0=Entsperre alle Schlüssel von Standard-Eingabe mithilfe der alten Passwörter und gebe sie mit dem neuen Passwort gesperrt auf Standard-Ausgabe aus. +usage.description.1=Falls einer oder mehrere (Unter-)Schlüssel nicht entsperrt werden können, gibt diese Operation den Fehlercode 67 aus. +no-armor=Schütze Ausgabe mit ASCII Armor +new-key-password.0=Neues Passwort zur Sperrung der Schlüssel. +new-key-password.1=Falls kein neues Passwort angegeben wird, werden die Schlüssel entsperrt ausgegeben. +new-key-password.2=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +old-key-password.0=Alte Passwörter zum Entsperren der Schlüssel. +old-key-password.1=Mehrere Passwortkandidaten können übergeben werden, welche der Reihe nach durchprobiert werden, um Unterschlüssel zu entsperren. +old-key-password.2=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). + +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.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 new file mode 100644 index 0000000..55cbf45 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_dearmor.properties @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# 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.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 new file mode 100644 index 0000000..e01ab7a --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_dearmor_de.properties @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# 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.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 new file mode 100644 index 0000000..bec315f --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_decrypt.properties @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Decrypt a message +session-key-out=Can be used to learn the session key on successful decryption +with-session-key.0=Symmetric message key (session key). +with-session-key.1=Enables decryption of the "CIPHERTEXT" using the session key directly against the "SEIPD" packet. +with-session-key.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +with-password.0=Symmetric passphrase to decrypt the message with. +with-password.1=Enables decryption based on any "SKESK" packets in the "CIPHERTEXT". +with-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +verify-out=Emits signature verification status to the designated output +verify-with=Certificates for signature verification +verify-not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) +verify-not-before.1=Reject signatures with a creation date not in range. +verify-not-before.2=Defaults to beginning of time ('-'). +verify-not-after.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) +verify-not-after.1=Reject signatures with a creation date not in range. +verify-not-after.2=Defaults to current system time ('now'). +verify-not-after.3=Accepts special value '-' for end of time. +with-key-password.0=Passphrase to unlock the secret key(s). +with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +KEY[0..*]=Secret keys to attempt decryption with + +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.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 new file mode 100644 index 0000000..395a89f --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_decrypt_de.properties @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Entschlüssle eine Nachricht +session-key-out=Extrahiere den Nachrichtenschlüssel nach erfolgreicher Entschlüsselung +with-session-key.0=Symmetrischer Nachrichtenschlüssel (Sitzungsschlüssel). +with-session-key.1=Ermöglicht direkte Entschlüsselung des im "CIPHERTEXT" enthaltenen "SEIPD" Paketes mithilfe des Nachrichtenschlüssels. +with-session-key.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +with-password.0=Symmetrisches Passwort zur Entschlüsselung der Nachricht. +with-password.1=Ermöglicht Entschlüsselung basierend auf im "CIPHERTEXT" enthaltenen "SKESK" Paketen. +with-password.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +verify-out=Schreibe Status der Signaturprüfung in angegebene Ausgabe +verify-with=Zertifikate zur Signaturprüfung +verify-not-before.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) +verify-not-before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. +verify-not-before.2=Standardmäßig: Anbeginn der Zeit ('-'). +verify-not-after.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) +verify-not-after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. +verify-not-after.2=Standardmäßig: Aktueller Zeitpunkt ('now'). +verify-not-after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten. +with-key-password.0=Passwort zum Entsperren der privaten Schlüssel +with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +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.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 new file mode 100644 index 0000000..6ebfd0b --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_detached-sign.properties @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Create a detached message signature +no-armor=ASCII armor the output +as.0=Specify the output format of the signed message. +as.1=Defaults to 'binary'. +as.2=If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53. +with-key-password.0=Passphrase to unlock the secret key(s). +with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +micalg-out=Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156). +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.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 new file mode 100644 index 0000000..39b59b5 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_detached-sign_de.properties @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Erstelle abgetrennte Nachrichten-Signatur +no-armor=Schütze Ausgabe mit ASCII Armor +as.0=Bestimme Signaturformat der Nachricht. +as.1=Standardmäßig: 'binary'. +as.2=Ist die Standard-Eingabe nicht UTF-8 kodiert und '--as=text' gesetzt, so wird inline-sign Fehlercode 53 zurückgeben. +with-key-password.0=Passwort zum Entsperren des privaten Schlüssels +with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +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.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 new file mode 100644 index 0000000..074a318 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_detached-verify.properties @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Verify a detached signature +usage.description=Verify a detached signature over some data from STDIN. +not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) +not-before.1=Reject signatures with a creation date not in range. +not-before.2=Defaults to beginning of time ("-"). +not-after.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) +not-after.1=Reject signatures with a creation date not in range. +not-after.2=Defaults to current system time ("now"). +not-after.3=Accepts special value "-" for end of time. +SIGNATURE[0]=Detached signature +CERT[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.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 new file mode 100644 index 0000000..e21ee2a --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_detached-verify_de.properties @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Prüfe eine abgetrennte Nachrichten-Signatur +usage.description=Prüfe eine abgetrennte Signatur über eine Nachricht von Standard-Eingabe. +not-before.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) +not-before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. +not-before.2=Standardmäßig: Anbeginn der Zeit ('-'). +not-after.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) +not-after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. +not-after.2=Standardmäßig: Aktueller Zeitpunkt ('now'). +not-after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten. +SIGNATURE[0]=Abgetrennte Signatur +CERT[1..*]=Zertifikate (öffentliche Schlüssel) zur Signaturprüfung + +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.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 new file mode 100644 index 0000000..7bbf593 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_encrypt.properties @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Encrypt a message from standard input +no-armor=ASCII armor the output +as=Type of the input data. Defaults to 'binary' +with-password.0=Encrypt the message with a password. +with-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +sign-with=Sign the output with a private key +profile=Profile identifier to switch between profiles +with-key-password.0=Passphrase to unlock the secret key(s). +with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +CERTS[0..*]=Certificates the message gets encrypted to + +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.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 new file mode 100644 index 0000000..55b0338 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_encrypt_de.properties @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Verschlüssle eine Nachricht von Standard-Eingabe +no-armor=Schütze Ausgabe mit ASCII Armor +as=Format der Nachricht. Standardmäßig 'binary' +with-password.0=Verschlüssle die Nachricht mit einem Passwort +with-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +sign-with=Signiere die Nachricht mit einem privaten Schlüssel +profile=Profil-Identifikator um zwischen Profilen zu wechseln +with-key-password.0=Passwort zum Entsperren der privaten Schlüssel +with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +CERTS[0..*]=Zertifikate für die die Nachricht verschlüsselt werden soll + +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.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 new file mode 100644 index 0000000..1d1dee4 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_extract-cert.properties @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +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.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 new file mode 100644 index 0000000..c92d31d --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_extract-cert_de.properties @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Extrahiere Zertifikat (öffentlichen Schlüssel) aus privatem Schlüssel +usage.description=Lese einen Schlüssel von Standard-Eingabe und gebe das Zertifikat auf Standard-Ausgabe aus. +no-armor=Schütze Ausgabe mit ASCII Armor + +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.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 new file mode 100644 index 0000000..c17f7f6 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_generate-key.properties @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Generate a secret key +no-armor=ASCII armor the output +USERID[0..*]=User-ID, e.g. "Alice " +profile=Profile identifier to switch between profiles +signing-only=Generate a key that 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.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 new file mode 100644 index 0000000..84db04d --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_generate-key_de.properties @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Generiere einen privaten Schlüssel +no-armor=Schütze Ausgabe mit ASCII Armor +USERID[0..*]=Nutzer-ID, z.B.. "Alice " +profile=Profil-Identifikator um zwischen Profilen zu wechseln +signing-only=Generiere einen Schlüssel, der nur zum Signieren genutzt werden kann +with-key-password.0=Passwort zum Schutz des privaten Schlüssels +with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). + +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.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 new file mode 100644 index 0000000..637c1d0 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_help.properties @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Display usage information for the specified subcommand + +stacktrace=Print stacktrace +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.synopsisHeading=Usage:\u0020 +usage.commandListHeading=%nCommands:%n +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 new file mode 100644 index 0000000..8471188 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_help_de.properties @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Zeige Nutzungshilfen für den angegebenen Unterbefehl an + +stacktrace=Stacktrace ausgeben +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.synopsisHeading=Aufruf:\u0020 +usage.commandListHeading=%nBefehle:%n +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 new file mode 100644 index 0000000..ca0ed6b --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_inline-detach.properties @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +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.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 new file mode 100644 index 0000000..84b8c47 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_inline-detach_de.properties @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +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.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 new file mode 100644 index 0000000..936b417 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_inline-sign.properties @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Create an inline-signed message +no-armor=ASCII armor the output +as.0=Specify the signature format of the signed message. +as.1='text' and 'binary' will produce inline-signed messages. +as.2='clearsigned' will make use of the cleartext signature framework. +as.3=Defaults to 'binary'. +as.4=If '--as=text' and the input data is not valid UTF-8, inline-sign fails with return code 53. +with-key-password.0=Passphrase to unlock the secret key(s). +with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +micalg=Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156). +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.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 new file mode 100644 index 0000000..f8fe906 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_inline-sign_de.properties @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Signiere eine Nachricht mit eingebetteten Signaturen +no-armor=Schütze Ausgabe mit ASCII Armor +as.0=Bestimme Signaturformat der Nachricht. +as.1='text' und 'binary' resultieren in eingebettete Signaturen. +as.2='clearsigned' wird die Nachricht Klartext-signieren. +as.3=Standardmäßig: 'binary'. +as.4=Ist die Standard-Eingabe nicht UTF-8 kodiert und '--as=text' gesetzt, so wird inline-sign Fehlercode 53 zurückgeben. +with-key-password.0=Passwort zum Entsperren des privaten Schlüssels +with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +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.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 new file mode 100644 index 0000000..2e0d69f --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_inline-verify.properties @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Verify an inline-signed message +not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) +not-before.1=Reject signatures with a creation date not in range. +not-before.2=Defaults to beginning of time ("-"). +not-after.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) +not-after.1=Reject signatures with a creation date not in range. +not-after.2=Defaults to current system time ("now"). +not-after.3=Accepts special value "-" for end of time. +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.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 new file mode 100644 index 0000000..9b70504 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_inline-verify_de.properties @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Prüfe eingebettete Signaturen einer Nachricht +not-before.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) +not-before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. +not-before.2=Standardmäßig: Anbeginn der Zeit ('-'). +not-after.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) +not-after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. +not-after.2=Standardmäßig: Aktueller Zeitpunkt ('now'). +not-after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten. +verifications-out=Schreibe Status der Signaturprüfung in angegebene Ausgabe +CERT[0..*]=Zertifikate (öffentlich Schlüssel) zur Signaturprüfung + +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.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 new file mode 100644 index 0000000..3defe8e --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_list-profiles.properties @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2023 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Emit a list of profiles supported by the identified subcommand +subcommand=Subcommand for which to list profiles + +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.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 new file mode 100644 index 0000000..093aeb3 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_list-profiles_de.properties @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2023 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Gebe eine Liste von Profilen aus, welche vom angegebenen Unterbefehl unterstützt werden +subcommand=Unterbefehl, für welchen Profile gelistet werden sollen + +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.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 new file mode 100644 index 0000000..8c0bfa3 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_merge-certs.properties @@ -0,0 +1,22 @@ +# 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 new file mode 100644 index 0000000..b1f008c --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.headerHeading=OpenPGP Zertifikate zusammenführen%n +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 new file mode 100644 index 0000000..f68b774 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_revoke-key.properties @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2023 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Generate revocation certificates +usage.description=Emit revocation certificates for secret keys from STDIN to STDOUT. +no-armor=ASCII armor the output +with-key-password.0=Passphrase to unlock the secret key(s). +with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). + +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.synopsisHeading=Usage:\u0020 +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n +usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties b/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties new file mode 100644 index 0000000..fa8c5b4 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2023 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Erzeuge Widerrufszertifikate +usage.description=Gebe Widerrufszertifikate für Schlüssel von Standard-Eingabe auf Standard-Ausgabe aus. +no-armor=Schütze Ausgabe mit ASCII Armor +with-key-password.0=Passwort zum Entsperren der privaten Schlüssel +with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). + +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.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 new file mode 100644 index 0000000..520533a --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# 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.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 +usage.exitCodeList.1=\u00201:Generic program error +usage.exitCodeList.2=\u00203:Verification requested but no verifiable signature found +usage.exitCodeList.3=13:Unsupported asymmetric algorithm +usage.exitCodeList.4=17:Certificate is not encryption capable +usage.exitCodeList.5=19:Usage error: Missing argument +usage.exitCodeList.6=23:Incomplete verification instructions +usage.exitCodeList.7=29:Unable to decrypt +usage.exitCodeList.8=31:Password is not human-readable +usage.exitCodeList.9=37:Unsupported Option +usage.exitCodeList.10=41:Invalid data or data of wrong type encountered +usage.exitCodeList.11=53:Non-text input received where text was expected +usage.exitCodeList.12=59:Output file already exists +usage.exitCodeList.13=61:Input file does not exist +usage.exitCodeList.14=67:Cannot unlock password protected secret key +usage.exitCodeList.15=69:Unsupported subcommand +usage.exitCodeList.16=71:Unsupported special prefix (e.g. \"@ENV/@FD\") of indirect parameter +usage.exitCodeList.17=73:Ambiguous input (a filename matching the designator already exists) +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 + +## Malformed Input +sop.error.input.malformed_session_key=Session keys are expected in the format 'ALGONUM:HEXKEY'. +sop.error.input.not_a_private_key=Input '%s' does not contain an OpenPGP private key. +sop.error.input.not_a_certificate=Input '%s' does not contain an OpenPGP certificate. +sop.error.input.not_a_signature=Input '%s' does not contain an OpenPGP signature. +sop.error.input.malformed_not_after=Invalid date string supplied as value of '--not-after'. +sop.error.input.malformed_not_before=Invalid date string supplied as value of '--not-before'. +sop.error.input.stdin_not_a_message=Standard Input appears not to contain a valid OpenPGP message. +sop.error.input.stdin_not_a_private_key=Standard Input appears not to contain a valid OpenPGP secret key. +sop.error.input.stdin_not_openpgp_data=Standard Input appears not to contain valid OpenPGP data. +## Indirect Data Types +sop.error.indirect_data_type.ambiguous_filename=File name '%s' is ambiguous. File with the same name exists on the filesystem. +sop.error.indirect_data_type.environment_variable_not_set=Environment variable '%s' not set. +sop.error.indirect_data_type.environment_variable_empty=Environment variable '%s' is empty. +sop.error.indirect_data_type.file_descriptor_not_found=File descriptor '%s' not found. +sop.error.indirect_data_type.input_file_does_not_exist=Input file '%s' does not exist. +sop.error.indirect_data_type.input_not_a_file=Input file '%s' is not a file. +sop.error.indirect_data_type.output_file_already_exists=Output file '%s' already exists. +sop.error.indirect_data_type.output_file_cannot_be_created=Output file '%s' cannot be created. +sop.error.indirect_data_type.illegal_use_of_env_designator=Special designator '@ENV' cannot be used for output. +sop.error.indirect_data_type.designator_env_not_supported=Special designator '@ENV' is not supported. +sop.error.indirect_data_type.designator_fd_not_supported=Special designator '@FD' is not supported. +## Runtime Errors +sop.error.runtime.no_backend_set=No SOP backend set. +sop.error.runtime.cannot_unlock_key=Cannot unlock password-protected secret key from input '%s'. +sop.error.runtime.key_uses_unsupported_asymmetric_algorithm=Secret key from input '%s' uses an unsupported asymmetric algorithm. +sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm=Certificate from input '%s' uses an unsupported asymmetric algorithm. +sop.error.runtime.key_cannot_sign=Secret key from input '%s' cannot sign. +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. +sop.error.usage.parameter_required=Parameter '%s' is required. +sop.error.usage.profile_not_supported=Subcommand '%s' does not support profile '%s'. +sop.error.usage.option_requires_other_option=Option '%s' is requested, but no option %s was provided. +sop.error.usage.incompatible_options.clearsigned_no_armor=Options '--no-armor' and '--as=clearsigned' are incompatible. +# Feature Support +sop.error.feature_support.subcommand_not_supported=Subcommand '%s' is not supported. +sop.error.feature_support.option_not_supported=Option '%s' not supported. +sop.error.feature_support.subcommand_does_not_support_profiles=Subcommand '%s' does not support any profiles. diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties new file mode 100644 index 0000000..99d28a7 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# 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.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 +usage.exitCodeList.1=\u00201:Generischer Programmfehler +usage.exitCodeList.2=\u00203:Signaturverifikation gefordert, aber keine gültige Signatur gefunden +usage.exitCodeList.3=13:Nicht unterstützter asymmetrischer Algorithmus +usage.exitCodeList.4=17:Dem Zertifikat ist es nicht erlaubt zu verschlüsseln +usage.exitCodeList.5=19:Nutzungsfehler: Fehlendes Argument +usage.exitCodeList.6=23:Unvollständige Verifikationsanweisungen +usage.exitCodeList.7=29:Entschlüsselung nicht möglich +usage.exitCodeList.8=31:Passwort ist nicht für Menschen lesbar +usage.exitCodeList.9=37:Nicht unterstützte Option +usage.exitCodeList.10=41:Ungültige Daten oder Daten des falschen Typs gefunden +usage.exitCodeList.11=53:Nicht-Text-Eingabe erhalten, wo Text erwartet wurde +usage.exitCodeList.12=59:Ausgabedatei existiert bereits +usage.exitCodeList.13=61:Eingabedatei existiert nicht +usage.exitCodeList.14=67:Passwort-gesicherter privater Schlüssel kann nicht entsperrt werden +usage.exitCodeList.15=69:Nicht unterstützter Unterbefehl +usage.exitCodeList.16=71:Nicht unterstützter Spezialprefix (z.B.. "@ENV/@FD") von indirektem Parameter +usage.exitCodeList.17=73:Mehrdeutige Eingabe (ein Dateiname, der dem Bezeichner entspricht, existiert bereits) +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 +## Malformed Input +sop.error.input.malformed_session_key=Nachrichtenschlüssel werden im folgenden Format erwartet: 'ALGONUM:HEXKEY' +sop.error.input.not_a_private_key=Eingabe '%s' enthält keinen privaten OpenPGP Schlüssel. +sop.error.input.not_a_certificate=Eingabe '%s' enthält kein OpenPGP Zertifikat. +sop.error.input.not_a_signature=Eingabe '%s' enthält keine OpenPGP Signatur. +sop.error.input.malformed_not_after=Ungültige Datumszeichenfolge als Wert von '--not-after'. +sop.error.input.malformed_not_before=Ungültige Datumszeichenfolge als Wert von '--not-before'. +sop.error.input.stdin_not_a_message=Standard-Eingabe enthält scheinbar keine OpenPGP Nachricht. +sop.error.input.stdin_not_a_private_key=Standard-Eingabe enthält scheinbar keinen privaten OpenPGP Schlüssel. +sop.error.input.stdin_not_openpgp_data=Standard-Eingabe enthält scheinbar keine gültigen OpenPGP Daten. +## Indirect Data Types +sop.error.indirect_data_type.ambiguous_filename=Dateiname '%s' ist mehrdeutig. Datei mit dem selben Namen existiert im Dateisystem. +sop.error.indirect_data_type.environment_variable_not_set=Umgebungsvariable '%s' nicht gesetzt. +sop.error.indirect_data_type.environment_variable_empty=Umgebungsvariable '%s' ist leer. +sop.error.indirect_data_type.file_descriptor_not_found=File Descriptor '%s' nicht gefunden. +sop.error.indirect_data_type.input_file_does_not_exist=Quelldatei '%s' existiert nicht. +sop.error.indirect_data_type.input_not_a_file=Quelldatei '%s' ist keine Datei. +sop.error.indirect_data_type.output_file_already_exists=Zieldatei '%s' existiert bereits. +sop.error.indirect_data_type.output_file_cannot_be_created=Zieldatei '%s' kann nicht erstellt werden. +sop.error.indirect_data_type.illegal_use_of_env_designator=Besonderer Bezeichner-Präfix '@ENV' darf nicht für Ausgaben verwendet werden. +sop.error.indirect_data_type.designator_env_not_supported=Besonderer Bezeichner-Präfix '@ENV' wird nicht unterstützt. +sop.error.indirect_data_type.designator_fd_not_supported=Besonderer Bezeichner-Präfix '@FD' wird nicht unterstützt. +## Runtime Errors +sop.error.runtime.no_backend_set=Kein SOP Backend gesetzt. +sop.error.runtime.cannot_unlock_key=Gesperrter Schlüssel aus Eingabe '%s' kann nicht entsperrt werden. +sop.error.runtime.key_uses_unsupported_asymmetric_algorithm=Privater Schlüssel aus Eingabe '%s' nutzt nicht unterstütztem asymmetrischen Algorithmus. +sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm=Zertifikat aus Eingabe '%s' nutzt nicht unterstütztem asymmetrischen Algorithmus. +sop.error.runtime.key_cannot_sign=Privater Schlüssel aus Eingabe '%s' kann nicht signieren. +sop.error.runtime.cert_cannot_encrypt=Zertifikat aus Eingabe '%s' kann nicht verschlüsseln. +sop.error.runtime.no_session_key_extracted=Nachrichtenschlüssel nicht extrahiert. Funktion wird möglicherweise nicht unterstützt. +sop.error.runtime.no_verifiable_signature_found=Keine gültigen Signaturen gefunden. +sop.error.runtime.cannot_decrypt_message=Nachricht konnte nicht entschlüsselt werden. +## Usage errors +sop.error.usage.password_or_cert_required=Es wird mindestens ein Passwort und/oder Zertifikat zur Verschlüsselung benötigt. +sop.error.usage.argument_required=Argument '%s' ist erforderlich. +sop.error.usage.parameter_required=Parameter '%s' ist erforderlich. +sop.error.usage.profile_not_supported=Unterbefehl '%s' unterstützt Profil '%s' nicht. +sop.error.usage.option_requires_other_option=Option '%s' wurde angegeben, jedoch kein Wert für %s. +sop.error.usage.incompatible_options.clearsigned_no_armor=Optionen '--no-armor' und '--as=clearsigned' sind inkompatibel. +# Feature Support +sop.error.feature_support.subcommand_not_supported=Unterbefehl '%s' wird nicht unterstützt. +sop.error.feature_support.option_not_supported=Option '%s' wird nicht unterstützt. +sop.error.feature_support.subcommand_does_not_support_profiles=Unterbefehl '%s' unterstützt keine Profile. diff --git a/sop-java-picocli/src/main/resources/msg_update-key.properties b/sop-java-picocli/src/main/resources/msg_update-key.properties new file mode 100644 index 0000000..0b5243e --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_update-key.properties @@ -0,0 +1,24 @@ +# 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 new file mode 100644 index 0000000..91e5532 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_update-key_de.properties @@ -0,0 +1,21 @@ +# 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 new file mode 100644 index 0000000..d25fa3a --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_validate-userid.properties @@ -0,0 +1,20 @@ +# 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 new file mode 100644 index 0000000..f919465 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties @@ -0,0 +1,20 @@ +# 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 new file mode 100644 index 0000000..1327a78 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_version.properties @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +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.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 new file mode 100644 index 0000000..c99045c --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_version_de.properties @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2022 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.header=Zeige Versionsinformationen über das Programm +extended=Gebe erweiterte Versionsinformationen aus +backend=Gebe Informationen über das kryptografische Backend aus +sop-spec=Gebe die neuste Revision der SOP Spezifikation aus, welche von dieser Implementierung umgesetzt wird +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.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/sop.properties b/sop-java-picocli/src/main/resources/sop.properties deleted file mode 100644 index 68f5a45..0000000 --- a/sop-java-picocli/src/main/resources/sop.properties +++ /dev/null @@ -1,157 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Paul Schaub -# -# SPDX-License-Identifier: Apache-2.0 -sop.name=sop -usage.header=Stateless OpenPGP Protocol - -usage.footerHeading=Powered by picocli%n -sop.locale=Locale for description texts -# Generic -usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -# Exit Codes -usage.exitCodeListHeading=%nExit Codes:%n -usage.exitCodeList.0=\u00200:Successful program execution. -usage.exitCodeList.1=\u00201:Generic program error -usage.exitCodeList.2=\u00203:Verification requested but no verifiable signature found -usage.exitCodeList.3=13:Unsupported asymmetric algorithm -usage.exitCodeList.4=17:Certificate is not encryption capable -usage.exitCodeList.5=19:Usage error: Missing argument -usage.exitCodeList.6=23:Incomplete verification instructions -usage.exitCodeList.7=29:Unable to decrypt -usage.exitCodeList.8=31:Password is not human-readable -usage.exitCodeList.9=37:Unsupported Option -usage.exitCodeList.10=41:Invalid data or data of wrong type encountered -usage.exitCodeList.11=53:Non-text input received where text was expected -usage.exitCodeList.12=59:Output file already exists -usage.exitCodeList.13=61:Input file does not exist -usage.exitCodeList.14=67:Cannot unlock password protected secret key -usage.exitCodeList.15=69:Unsupported subcommand -usage.exitCodeList.16=71:Unsupported special prefix (e.g. \"@env/@fd\") of indirect parameter -usage.exitCodeList.17=73:Ambiguous input (a filename matching the designator already exists) -usage.exitCodeList.18=79:Key is not signing capable -# Subcommands -sop.armor.usage.header=Add ASCII Armor to standard input -sop.armor.usage.option.label=Label to be used in the header and tail of the armoring -sop.dearmor.usage.header=Remove ASCII Armor from standard input -sop.decrypt.usage.header=Decrypt a message from standard input -sop.decrypt.usage.option.session_key_out=Can be used to learn the session key on successful decryption -sop.decrypt.usage.option.with_session_key.0=Symmetric message key (session key). -sop.decrypt.usage.option.with_session_key.1=Enables decryption of the "CIPHERTEXT" using the session key directly against the "SEIPD" packet. -sop.decrypt.usage.option.with_session_key.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) -sop.decrypt.usage.option.with_password.0=Symmetric passphrase to decrypt the message with. -sop.decrypt.usage.option.with_password.1=Enables decryption based on any "SKESK" packets in the "CIPHERTEXT". -sop.decrypt.usage.option.with_password_2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) -sop.decrypt.usage.option.verify_out=Emits signature verification status to the designated output -sop.decrypt.usage.option.certs=Certificates for signature verification -sop.decrypt.usage.option.not_before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) -sop.decrypt.usage.option.not_before.1=Reject signatures with a creation date not in range. -sop.decrypt.usage.option.not_before.2=Defaults to beginning of time ('-'). -sop.decrypt.usage.option.not_after.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) -sop.decrypt.usage.option.not_after.1=Reject signatures with a creation date not in range. -sop.decrypt.usage.option.not_after.2=Defaults to current system time ('now'). -sop.decrypt.usage.option.not_after.3=Accepts special value '-' for end of time. -sop.decrypt.usage.option.with_key_password.0=Passphrase to unlock the secret key(s). -sop.decrypt.usage.option.with_key_password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). -sop.decrypt.usage.param.keys=Secret keys to attempt decryption with -sop.encrypt.usage.header=Encrypt a message from standard input -sop.encrypt.usage.option.armor=ASCII armor the output -sop.encrypt.usage.option.type=Type of the input data. Defaults to 'binary' -sop.encrypt.usage.option.with_password.0=Encrypt the message with a password. -sop.encrypt.usage.option.with_password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...) -sop.encrypt.usage.option.sign_with=Sign the output with a private key -sop.encrypt.usage.option.with_key_password.0=Passphrase to unlock the secret key(s). -sop.encrypt.usage.option.with_key_password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). -sop.encrypt.usage.param.certs=Certificates the message gets encrypted to -sop.extract-cert.usage.header=Extract a public key certificate from a secret key from standard input -sop.extract-cert.usage.option.armor=ASCII armor the output -sop.generate-key.usage.header=Generate a secret key -sop.generate-key.usage.option.armor=ASCII armor the output -sop.generate-key.usage.option.user_id=User-ID, e.g. "Alice " -sop.generate-key.usage.option.with_key_password.0=Password to protect the private key with -sop.generate-key.usage.option.with_key_password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). -sop.inline-detach.usage.header=Split signatures from a clearsigned message -sop.inline-detach.usage.option.armor=ASCII armor the output -sop.inline-detach.usage.option.signatures_out=Destination to which a detached signatures block will be written -sop.inline-sign.usage.header=Create an inline-signed message from data on standard input -sop.inline-sign.usage.option.armor=ASCII armor the output -sop.inline-sign.usage.option.as.0=Specify the signature format of the signed message -sop.inline-sign.usage.option.as.1='text' and 'binary' will produce inline-signed messages. -sop.inline-sign.usage.option.as.2='cleartextsigned' will make use of the cleartext signature framework. -sop.inline-sign.usage.option.as.3=Defaults to 'binary'. -sop.inline-sign.usage.option.as.4=If '--as=text' and the input data is not valid UTF-8, inline-sign fails with return code 53. -sop.inline-sign.usage.option.with_key_password.0=Passphrase to unlock the secret key(s). -sop.inline-sign.usage.option.with_key_password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). -sop.inline-sign.usage.option.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) -sop.inline-sign.usage.parameter.keys=Secret keys used for signing -sop.inline-verify.usage.header=Verify inline-signed data from standard input -sop.inline-verify.usage.option.not_before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) -sop.inline-verify.usage.option.not_before.1=Reject signatures with a creation date not in range. -sop.inline-verify.usage.option.not_before.2=Defaults to beginning of time ("-"). -sop.inline-verify.usage.option.not_after.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) -sop.inline-verify.usage.option.not_after.1=Reject signatures with a creation date not in range. -sop.inline-verify.usage.option.not_after.2=Defaults to current system time ("now"). -sop.inline-verify.usage.option.not_after.3=Accepts special value "-" for end of time. -sop.inline-verify.usage.option.verifications_out=File to write details over successful verifications to -sop.inline-verify.usage.parameter.certs=Public key certificates for signature verification -sop.sign.usage.header=Create a detached signature on the data from standard input -sop.sign.usage.option.armor=ASCII armor the output -sop.sign.usage.option.as.0=Specify the output format of the signed message -sop.sign.usage.option.as.1=Defaults to 'binary'. -sop.sign.usage-option.as.2=If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53. -sop.sign.usage.option.with_key_password.0=Passphrase to unlock the secret key(s). -sop.sign.usage.option.with_key_password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). -sop.sign.usage.option.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) -sop.sign.usage.parameter.keys=Secret keys used for signing -sop.verify.usage.header=Verify a detached signature over the data from standard input -sop.verify.usage.option.not_before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) -sop.verify.usage.option.not_before.1=Reject signatures with a creation date not in range. -sop.verify.usage.option.not_before.2=Defaults to beginning of time ("-"). -sop.verify.usage.option.not_after.1=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) -sop.verify.usage.option.not_after.2=Reject signatures with a creation date not in range. -sop.verify.usage.option.not_after.3=Defaults to current system time ("now").\ -sop.verify.usage.option.not_after.4 = Accepts special value "-" for end of time. -sop.verify.usage.parameter.signature=Detached signature -sop.verify.usage.parameter.certs=Public key certificates for signature verification -sop.version.usage.header=Display version information about the tool -sop.version.usage.option.extended=Print an extended version string -sop.version.usage.option.backend=Print information about the cryptographic backend -sop.help.usage.header=Display usage information for the specified subcommand -## Malformed Input -sop.error.input.malformed_session_key=Session keys are expected in the format 'ALGONUM:HEXKEY'. -sop.error.input.not_a_private_key=Input '%s' does not contain an OpenPGP private key. -sop.error.input.not_a_certificate=Input '%s' does not contain an OpenPGP certificate. -sop.error.input.not_a_signature=Input '%s' does not contain an OpenPGP signature. -sop.error.input.malformed_not_after=Invalid date string supplied as value of '--not-after'. -sop.error.input.malformed_not_before=Invalid date string supplied as value of '--not-before'. -sop.error.input.stdin_not_a_message=Standard Input appears not to contain a valid OpenPGP message. -sop.error.input.stdin_not_a_private_key=Standard Input appears not to contain a valid OpenPGP secret key. -sop.error.input.stdin_not_openpgp_data=Standard Input appears not to contain valid OpenPGP data -## Indirect Data Types -sop.error.indirect_data_type.ambiguous_filename=File name '%s' is ambiguous. File with the same name exists on the filesystem. -sop.error.indirect_data_type.environment_variable_not_set=Environment variable '%s' not set. -sop.error.indirect_data_type.environment_variable_empty=Environment variable '%s' is empty. -sop.error.indirect_data_type.input_file_does_not_exist=Input file '%s' does not exist. -sop.error.indirect_data_type.input_not_a_file=Input file '%s' is not a file. -sop.error.indirect_data_type.output_file_already_exists=Output file '%s' already exists. -sop.error.indirect_data_type.output_file_cannot_be_created=Output file '%s' cannot be created. -sop.error.indirect_data_type.illegal_use_of_env_designator=Special designator '@ENV:' cannot be used for output. -sop.error.indirect_data_type.designator_env_not_supported=Special designator '@ENV' is not supported. -sop.error.indirect_data_type.designator_fd_not_supported=Special designator '@FD' is not supported. -## Runtime Errors -sop.error.runtime.no_backend_set=No SOP backend set. -sop.error.runtime.cannot_unlock_key=Cannot unlock password-protected secret key from input '%s'. -sop.error.runtime.key_uses_unsupported_asymmetric_algorithm=Secret key from input '%s' uses an unsupported asymmetric algorithm. -sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm=Certificate from input '%s' uses an unsupported asymmetric algorithm. -sop.error.runtime.key_cannot_sign=Secret key from input '%s' cannot sign. -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. -## 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. -sop.error.usage.parameter_required=Parameter '%s' is required. -sop.error.usage.option_requires_other_option=Option '%s' is requested, but no option %s was provided. -# Feature Support -sop.error.feature_support.subcommand_not_supported=Subcommand '%s' is not supported. -sop.error.feature_support.option_not_supported=Option '%s' not supported. \ No newline at end of file diff --git a/sop-java-picocli/src/main/resources/sop_de.properties b/sop-java-picocli/src/main/resources/sop_de.properties deleted file mode 100644 index 6026f9a..0000000 --- a/sop-java-picocli/src/main/resources/sop_de.properties +++ /dev/null @@ -1,156 +0,0 @@ -# SPDX-FileCopyrightText: 2022 Paul Schaub -# -# SPDX-License-Identifier: Apache-2.0 -sop.name=sop -usage.header=Stateless OpenPGP Protocol -usage.footerHeading=Powered by Picocli%n -sop.locale=Gebietsschema für Beschreibungstexte -# Generic -usage.synopsisHeading=Aufruf:\u0020 -usage.commandListHeading=%nBefehle:%n -# Exit Codes -usage.exitCodeListHeading=%nExit Codes:%n -usage.exitCodeList.0=\u00200:Erfolgreiche Programmausführung -usage.exitCodeList.1=\u00201:Generischer Programmfehler -usage.exitCodeList.2=\u00203:Signaturverifikation gefordert, aber keine gültige Signatur gefunden -usage.exitCodeList.3=13:Nicht unterstützter asymmetrischer Algorithmus -usage.exitCodeList.4=17:Dem Zertifikat ist es nicht erlaubt zu verschlüsseln -usage.exitCodeList.5=19:Nutzungsfehler: Fehlendes Argument -usage.exitCodeList.6=23:Unvollständige Verifikationsanweisungen -usage.exitCodeList.7=29:Entschlüsselung nicht möglich -usage.exitCodeList.8=31:Passwort ist nicht für Menschen lesbar -usage.exitCodeList.9=37:Nicht unterstützte Option -usage.exitCodeList.10=41:Ungültige Daten oder Daten des falschen Typs gefunden -usage.exitCodeList.11=53:Nicht-Text-Eingabe erhalten, wo Text erwartet wurde -usage.exitCodeList.12=59:Ausgabedatei existiert bereits -usage.exitCodeList.13=61:Eingabedatei existiert nicht -usage.exitCodeList.14=67:Passwort-gesicherter privater Schlüssel kann nicht entsperrt werden -usage.exitCodeList.15=69:Nicht unterstützter Unterbefehl -usage.exitCodeList.16=71:Nicht unterstützter Spezialprefix (z.B.. "@env/@fd") von indirektem Parameter -usage.exitCodeList.17=73:Mehrdeutige Eingabe (ein Dateiname, der dem Bezeichner entspricht, existiert bereits) -usage.exitCodeList.18=79:Schlüssel ist nicht fähig zu signieren -# Subcommands -sop.armor.usage.header=Schütze Standard-Eingabe mit ASCII Armor -sop.armor.usage.option.label=Label für Kopf- und Fußzeile der ASCII Armor -sop.dearmor.usage.header=Entferne ASCII Armor von Standard-Eingabe -sop.decrypt.usage.header=Entschlüssle eine Nachricht von Standard-Eingabe -sop.decrypt.usage.option.session_key_out=Extrahiere den Nachrichtenschlüssel nach erfolgreicher Entschlüsselung -sop.decrypt.usage.option.with_session_key.0=Symmetrischer Nachrichtenschlüssel (Sitzungsschlüssel). -sop.decrypt.usage.option.with_session_key.1=Ermöglicht direkte Entschlüsselung des im "CIPHERTEXT" enhaltenen "SEIPD" Paketes mithilfe des Nachrichtenschlüssels. -sop.decrypt.usage.option.with_session_key.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). -sop.decrypt.usage.option.with_password.0=Symmetrisches Passwort zur Entschlüsselung der Nachricht. -sop.decrypt.usage.option.with_password.1=Ermöglicht Entschlüsselung basierend auf im "CIPHERTEXT" enthaltenen "SKESK" Paketen. -sop.decrypt.usage.option.with_password.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). -sop.decrypt.usage.option.verify_out=Schreibe Status der Signaturprüfung in angegebene Ausgabe -sop.decrypt.usage.option.certs=Zertifikate zur Signaturprüfung -sop.decrypt.usage.option.not_before.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) -sop.decrypt.usage.option.not_before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. -sop.decrypt.usage.option.not_before.2=Standardmäßig: Anbeginn der Zeit ('-'). -sop.decrypt.usage.option.not_after.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) -sop.decrypt.usage.option.not_after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. -sop.decrypt.usage.option.not_after.2=Standardmäßig: Aktueller Zeitunkt ('now'). -sop.decrypt.usage.option.not_after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten. -sop.decrypt.usage.option.with_key_password.0=Passwort zum Entsperren der privaten Schlüssel -sop.decrypt.usage.option.with_key_password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). -sop.decrypt.usage.param.keys=Private Schlüssel zum Entschlüsseln der Nachricht -sop.encrypt.usage.header=Verschlüssle eine Nachricht von Standard-Eingabe -sop.encrypt.usage.option.armor=Schütze Ausgabe mit ASCII Armor -sop.encrypt.usage.option.type=Format der Nachricht. Standardmäßig 'binary' -sop.encrypt.usage.option.with_password.0=Verschlüssle die Nachricht mit einem Passwort -sop.encrypt.usage.option.with_password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). -sop.encrypt.usage.option.sign_with=Signiere die Nachricht mit einem privaten Schlüssel -sop.encrypt.usage.option.with_key_password.0=Passwort zum Entsperren der privaten Schlüssel -sop.encrypt.usage.option.with_key_password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). -sop.encrypt.usage.param.certs=Zertifikate für die die Nachricht verschlüsselt werden soll -sop.extract-cert.usage.header=Extrahiere Zertifikat (öffentlichen Schlüssel) aus privatem Schlüssel von Standard-Eingabe -sop.extract-cert.usage.option.armor=Schütze Ausgabe mit ASCII Armor -sop.generate-key.usage.header=Generiere einen privaten Schlüssel -sop.generate-key.usage.option.armor=Schütze Ausgabe mit ASCII Armor -sop.generate-key.usage.option.user_id=Nutzer-ID, z.B.. "Alice " -sop.generate-key.usage.option.with_key_password.0=Passwort zum Schutz des privaten Schlüssels -sop.generate-key.usage.option.with_key_password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). -sop.inline-detach.usage.header=Trenne Signaturen von Klartext-signierter Nachricht -sop.inline-detach.usage.option.armor=Schütze Ausgabe mit ASCII Armor -sop.inline-detach.usage.option.signatures_out=Schreibe abgetrennte Signaturen in Ausgabe -sop.inline-sign.usage.header=Signiere eine Nachricht von Standard-Eingabe mit eingebetteten Signaturen -sop.inline-sign.usage.option.armor=Schütze Ausgabe mit ASCII Armor -sop.inline-sign.usage.option.as.0=Bestimme Signaturformat der Nachricht. -sop.inline-sign.usage.option.as.1='text' und 'binary' resultieren in eingebettete Signaturen. -sop.inline-sign.usage.option.as.2='cleartextsigned' wird die Nachricht Klartext-signieren. -sop.inline-sign.usage.option.as.3=Standardmäßig: 'binary'. -sop.inline-sign.usage.option.as.4=Ist die Standard-Eingabe nicht UTF-8 kodiert und '--as=text' gesetzt, so wird inline-sign Fehlercode 53 zurückgeben. -sop.inline-sign.usage.option.with_key_password.0=Passwort zum Entsperren des privaten Schlüssels -sop.inline-sign.usage.option.with_key_password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). -sop.inline-sign.usage.option.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. -sop.inline-sign.usage.parameter.keys=Private Signaturschlüssel -sop.inline-verify.usage.header=Prüfe eingebettete Signaturen einer Nachricht von Standard-Eingabe -sop.inline-verify.usage.option.not_before.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) -sop.inline-verify.usage.option.not_before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. -sop.inline-verify.usage.option.not_before.2=Standardmäßig: Anbeginn der Zeit ('-'). -sop.inline-verify.usage.option.not_after.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) -sop.inline-verify.usage.option.not_after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. -sop.inline-verify.usage.option.not_after.2=Standardmäßig: Aktueller Zeitunkt ('now'). -sop.inline-verify.usage.option.not_after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten. -sop.inline-verify.usage.option.verifications_out=Schreibe Status der Signaturprüfung in angegebene Ausgabe -sop.inline-verify.usage.parameter.certs=Zertifikate (öffentlich Schlüssel) zur Signaturprüfung -sop.sign.usage.header=Erstelle abgetrennte Signatur über Nachricht von Standard-Eingabe -sop.sign.usage.option.armor=Schütze Ausgabe mit ASCII Armor -sop.sign.usage.option.as.0=Bestimme Signaturformat der Nachricht. -sop.sign.usage.option.as.1=Standardmäßig: 'binary'. -sop.sign.usage-option.as.2=Ist die Standard-Eingabe nicht UTF-8 kodiert und '--as=text' gesetzt, so wird inline-sign Fehlercode 53 zurückgeben. -sop.sign.usage.option.with_key_password.0=Passwort zum Entsperren des privaten Schlüssels -sop.sign.usage.option.with_key_password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). -sop.sign.usage.option.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. -sop.sign.usage.parameter.keys=Private Signaturschlüssel -sop.verify.usage.header=Prüfe eine abgetrennte Signatur über eine Nachricht von Standard-Eingabe -sop.verify.usage.option.not_before.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) -sop.verify.usage.option.not_before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. -sop.verify.usage.option.not_before.2=Standardmäßig: Anbeginn der Zeit ('-'). -sop.verify.usage.option.not_after.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z) -sop.verify.usage.option.not_after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab. -sop.verify.usage.option.not_after.2=Standardmäßig: Aktueller Zeitunkt ('now'). -sop.verify.usage.option.not_after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten. -sop.verify.usage.parameter.signature=Abgetrennte Signatur -sop.verify.usage.parameter.certs=Zertifikate (öffentliche Schlüssel) zur Signaturprüfung -sop.version.usage.header=Zeige Versionsinformationen über das Programm -sop.version.usage.option.extended=Gebe erweiterte Versionsinformationen aus -sop.version.usage.option.backend=Gebe Informationen über das kryptografische Backend aus -sop.help.usage.header=Zeige Nutzungshilfen für den angegebenen Unterbefehl an -## Malformed Input -sop.error.input.malformed_session_key=Nachrichtenschlüssel werden im folgenden Format erwartet: 'ALGONUM:HEXKEY' -sop.error.input.not_a_private_key=Eingabe '%s' enthält keinen privaten OpenPGP Schlüssel. -sop.error.input.not_a_certificate=Eingabe '%s' enthält kein OpenPGP Zertifikat. -sop.error.input.not_a_signature=Eingabe '%s' enthält keine OpenPGP Signatur. -sop.error.input.malformed_not_after=Ungültige Datumszeichenfolge als Wert von '--not-after'. -sop.error.input.malformed_not_before=Ungültige Datumszeichenfolge als Wert von '--not-before'. -sop.error.input.stdin_not_a_message=Standard-Eingabe enthält scheinbar keine OpenPGP Nachricht. -sop.error.input.stdin_not_a_private_key=Standard-Eingabe enthält scheinbar keinen privaten OpenPGP Schlüssel. -sop.error.input.stdin_not_openpgp_data=Standard-Eingabe enthält scheinbar keine gültigen OpenPGP Daten. -## Indirect Data Types -sop.error.indirect_data_type.ambiguous_filename=Dateiname '%s' ist mehrdeutig. Datei mit dem selben Namen existiert im Dateisystem. -sop.error.indirect_data_type.environment_variable_not_set=Umgebungsvariable '%s' nicht gesetzt. -sop.error.indirect_data_type.environment_variable_empty=Umgebungsvariable '%s' ist leer. -sop.error.indirect_data_type.input_file_does_not_exist=Quelldatei '%s' existiert nicht. -sop.error.indirect_data_type.input_not_a_file=Quelldatei '%s' ist keine Datei. -sop.error.indirect_data_type.output_file_already_exists=Zieldatei '%s' existiert bereits. -sop.error.indirect_data_type.output_file_cannot_be_created=Zieldatei '%s' kann nicht erstellt werden. -sop.error.indirect_data_type.illegal_use_of_env_designator=Besonderer Bezeichner-Präfix '@ENV:' darf nicht für Ausgaben verwendet werden. -sop.error.indirect_data_type.designator_env_not_supported=Besonderer Bezeichner-Präfix '@ENV' wird nicht unterstützt. -sop.error.indirect_data_type.designator_fd_not_supported=Besonderer Bezeichner-Präfix '@FD' wird nicht unterstützt. -## Runtime Errors -sop.error.runtime.no_backend_set=Kein SOP Backend gesetzt. -sop.error.runtime.cannot_unlock_key=Gesperrter Schlüssel aus Eingabe '%s' kann nicht entsperrt werden. -sop.error.runtime.key_uses_unsupported_asymmetric_algorithm=Privater Schlüssel aus Eingabe '%s' nutzt nicht unterstütztem asymmetrischen Algorithmus. -sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm=Zertifikat aus Eingabe '%s' nutzt nicht unterstütztem asymmetrischen Algorithmus. -sop.error.runtime.key_cannot_sign=Privater Schlüssel aus Eingabe '%s' kann nicht signieren. -sop.error.runtime.cert_cannot_encrypt=Zertifikat aus Eingabe '%s' kann nicht verschlüsseln. -sop.error.runtime.no_session_key_extracted=Nachrichtenschlüssel nicht extrahiert. Funktion wird möglicherweise nicht unterstützt. -sop.error.runtime.no_verifiable_signature_found=Keine gültigen Signaturen gefunden. -## Usage errors -sop.error.usage.password_or_cert_required=Es wird mindestens ein Passwort und/oder Zertifikat zur Verschlüsselung benötigt. -sop.error.usage.argument_required=Argument '%s' ist erforderlich. -sop.error.usage.parameter_required=Parameter '%s' ist erforderlich. -sop.error.usage.option_requires_other_option=Option '%s' wurde angegeben, jedoch kein Wert für %s. -# Feature Support -sop.error.feature_support.subcommand_not_supported=Unterbefehl '%s' wird nicht unterstützt. -sop.error.feature_support.option_not_supported=Option '%s' wird nicht unterstützt. \ No newline at end of file diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/DateParsingTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/DateParsingTest.java index 1fff1d5..e4550c7 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/DateParsingTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/DateParsingTest.java @@ -14,7 +14,7 @@ import sop.cli.picocli.commands.ArmorCmd; import sop.util.UTCUtil; public class DateParsingTest { - private AbstractSopCmd cmd = new ArmorCmd(); // we use ArmorCmd as a concrete implementation. + private final AbstractSopCmd cmd = new ArmorCmd(); // we use ArmorCmd as a concrete implementation. @Test public void parseNotAfterDashReturnsEndOfTime() { 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 9ca88da..62c7581 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java @@ -6,16 +6,19 @@ 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; import sop.operation.InlineDetach; @@ -26,30 +29,53 @@ import sop.operation.InlineSign; 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); - SopCLI.main(new String[] {"invalid"}); + assertUnsupportedSubcommand(() -> SopCLI.execute("invalid")); } @Test - @ExpectSystemExitWithStatus(1) public void assertThrowsIfNoSOPBackendSet() { - SopCLI.SOP_INSTANCE = null; - // At this point, no SOP backend is set, so an InvalidStateException triggers exit(1) - SopCLI.main(new String[] {"armor"}); + SopCLI.setSopInstance(null); + // At this point, no SOP backend is set, so an InvalidStateException triggers error code 1 + assertGenericError(() -> SopCLI.execute("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; @@ -95,6 +121,21 @@ public class SOPTest { return null; } + @Override + public ListProfiles listProfiles() { + return null; + } + + @Override + public RevokeKey revokeKey() { + return null; + } + + @Override + public ChangeKeyPassword changeKeyPassword() { + return null; + } + @Override public InlineDetach inlineDetach() { return null; @@ -123,6 +164,11 @@ public class SOPTest { commands.add(new String[] {"sign"}); commands.add(new String[] {"verify", "signature.asc", "cert.asc"}); commands.add(new String[] {"version"}); + commands.add(new String[] {"list-profiles", "generate-key"}); + commands.add(new String[] {"certify-userid", "--userid", "Alice ", "--", "alice.pgp"}); + commands.add(new String[] {"validate-userid", "Alice ", "bob.pgp", "--", "alice.pgp"}); + commands.add(new String[] {"update-key"}); + commands.add(new String[] {"merge-certs"}); for (String[] command : commands) { int exit = SopCLI.execute(command); diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/TestFileUtil.java b/sop-java-picocli/src/test/java/sop/cli/picocli/TestFileUtil.java index 385fe1a..d1dc5d1 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/TestFileUtil.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/TestFileUtil.java @@ -12,10 +12,15 @@ import java.nio.file.Files; public class TestFileUtil { - public static File writeTempStringFile(String string) throws IOException { - File tempDir = Files.createTempDirectory("tmpDir").toFile(); + public static File createTempDir() throws IOException { + File tempDir = Files.createTempDirectory("tmpFir").toFile(); tempDir.deleteOnExit(); tempDir.mkdirs(); + return tempDir; + } + + public static File writeTempStringFile(String string) throws IOException { + File tempDir = createTempDir(); File passwordFile = new File(tempDir, "file"); passwordFile.createNewFile(); 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 new file mode 100644 index 0000000..396bc7f --- /dev/null +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.opentest4j.TestAbortedException; +import sop.cli.picocli.TestFileUtil; +import sop.exception.SOPGPException; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class AbstractSopCmdTest { + + private static AbstractSopCmd abstractCmd; + private static final TestEnvironmentVariableResolver resolver = new TestEnvironmentVariableResolver(); + + @BeforeAll + public static void setup() { + abstractCmd = new VersionCmd(); // Use Version as representative command + abstractCmd.setEnvironmentVariableResolver(resolver); + } + + @Test + public void setEnvironmentVariableResolver_nullNPE() { + assertThrows(NullPointerException.class, () -> abstractCmd.setEnvironmentVariableResolver(null)); + } + + @Test + public void getInput_NullInvalid() { + assertThrows(NullPointerException.class, () -> abstractCmd.getInput(null)); + } + + @Test + public void getInput_EmptyInvalid() { + assertThrows(IllegalArgumentException.class, () -> abstractCmd.getInput("")); + } + + @Test + public void getInput_BlankInvalid() { + assertThrows(IllegalArgumentException.class, () -> abstractCmd.getInput(" ")); + } + + @Test + public void getInput_envNotSetIllegalArg() { + String envName = "@ENV:IS_NOT_SET"; + assertThrows(IllegalArgumentException.class, () -> abstractCmd.getInput(envName)); + } + + @Test + public void getInput_envEmptyIllegalArg() { + String envName = "@ENV:IS_EMPTY"; + resolver.addEnvironmentVariable("IS_EMPTY", ""); + assertThrows(IllegalArgumentException.class, () -> abstractCmd.getInput(envName)); + } + + @Test + public void getInput_fromEnv() throws IOException { + resolver.addEnvironmentVariable("FOO", "BAR"); + InputStream input = abstractCmd.getInput("@ENV:FOO"); + String string = readStringFromInputStream(input); + assertEquals("BAR", string); + } + + private static String readStringFromInputStream(InputStream input) + throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + byte[] buf = new byte[512]; + int read; + while ((read = input.read(buf)) > 0) { + output.write(buf, 0, read); + } + return output.toString(); + } + + @Test + public void getInput_envClashesWithExistingFile() throws IOException { + String env = "@ENV:existing.file"; + File tempFile = new File(env); + if (!tempFile.createNewFile()) { + throw new TestAbortedException("Cannot create temporary file " + tempFile.getAbsolutePath()); + } + tempFile.deleteOnExit(); + + resolver.addEnvironmentVariable("existing.file", "foo_bar"); + + assertThrows(SOPGPException.AmbiguousInput.class, () -> abstractCmd.getInput(env)); + } + + @Test + public void getInput_fdClashesWithExistingFile() throws IOException { + String env = "@FD:existing.file"; + File tempFile = new File(env); + if (!tempFile.createNewFile()) { + throw new TestAbortedException("Cannot create temporary file " + tempFile.getAbsolutePath()); + } + tempFile.deleteOnExit(); + + resolver.addEnvironmentVariable("existing.file", "foo_bar"); + + assertThrows(SOPGPException.AmbiguousInput.class, () -> abstractCmd.getInput(env)); + } + + @Test + public void getInput_missingFile() { + String missingFile = "missing.file"; + assertThrows(SOPGPException.MissingInput.class, () -> abstractCmd.getInput(missingFile)); + } + + @Test + public void getInput_notAFile() throws IOException { + File directory = TestFileUtil.createTempDir(); + directory.deleteOnExit(); + + assertThrows(SOPGPException.MissingInput.class, () -> abstractCmd.getInput(directory.getAbsolutePath())); + } + + @Test + public void getOutput_NullIllegalArg() { + assertThrows(IllegalArgumentException.class, () -> abstractCmd.getOutput(null)); + } + + @Test + public void getOutput_EmptyIllegalArg() { + assertThrows(IllegalArgumentException.class, () -> abstractCmd.getOutput("")); + } + + @Test + public void getOutput_BlankIllegalArg() { + assertThrows(IllegalArgumentException.class, () -> abstractCmd.getOutput(" ")); + } + + @Test + public void getOutput_envUnsupportedSpecialPrefix() { + assertThrows(SOPGPException.UnsupportedSpecialPrefix.class, () -> abstractCmd.getOutput("@ENV:IS_ILLEGAL")); + } + + @Test + public void getOutput_malformedFileDescriptor() { + assertThrows(IllegalArgumentException.class, () -> abstractCmd.getOutput("@FD:IS_ILLEGAL")); + } + + @Test + public void getOutput_fileExists() throws IOException { + File testFile = TestFileUtil.createTempDir(); + testFile.deleteOnExit(); + + assertThrows(SOPGPException.OutputExists.class, () -> abstractCmd.getOutput(testFile.getAbsolutePath())); + } +} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java index 6bdbe7f..3dd4c7c 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java @@ -4,28 +4,27 @@ 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; @@ -41,60 +40,30 @@ public class ArmorCmdTest { SopCLI.setSopInstance(sop); } - @Test - public void assertLabelIsNotCalledByDefault() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"armor"}); - verify(armor, never()).label(any()); - } - - @Test - public void assertLabelIsCalledWhenFlaggedWithArgument() throws SOPGPException.UnsupportedOption { - for (ArmorLabel label : ArmorLabel.values()) { - SopCLI.main(new String[] {"armor", "--label", label.name()}); - verify(armor, times(1)).label(label); - } - } - @Test public void assertDataIsAlwaysCalled() throws SOPGPException.BadData, IOException { - SopCLI.main(new String[] {"armor"}); + assertSuccess(() -> SopCLI.execute("armor")); verify(armor, times(1)).data((InputStream) any()); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void assertThrowsForInvalidLabel() { - SopCLI.main(new String[] {"armor", "--label", "Invalid"}); - } - - @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void ifLabelsUnsupportedExit37() throws SOPGPException.UnsupportedOption { - when(armor.label(any())).thenThrow(new SOPGPException.UnsupportedOption("Custom Armor labels are not supported.")); - - SopCLI.main(new String[] {"armor", "--label", "Sig"}); - } - - @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void ifBadDataExit41() throws SOPGPException.BadData, IOException { when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"armor"}); + assertBadData(() -> SopCLI.execute("armor")); } @Test - @FailOnSystemExit public void ifNoErrorsNoExit() { when(sop.armor()).thenReturn(armor); - SopCLI.main(new String[] {"armor"}); + assertSuccess(() -> SopCLI.execute("armor")); } private static Ready nopReady() { return new Ready() { @Override - public void writeTo(OutputStream outputStream) { + public void writeTo(@Nonnull 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 875eaed..b0a9fd8 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java @@ -9,12 +9,13 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.Ready; @@ -48,14 +49,13 @@ public class DearmorCmdTest { @Test public void assertDataIsCalled() throws IOException, SOPGPException.BadData { - SopCLI.main(new String[] {"dearmor"}); + assertSuccess(() -> SopCLI.execute("dearmor")); verify(dearmor, times(1)).data((InputStream) any()); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void assertBadDataCausesExit41() throws IOException, SOPGPException.BadData { when(dearmor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException("invalid armor"))); - SopCLI.main(new String[] {"dearmor"}); + assertBadData(() -> SopCLI.execute("dearmor")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java index 3da2d09..b7cb8bc 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java @@ -4,29 +4,6 @@ package sop.cli.picocli.commands; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -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 java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.Date; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatcher; @@ -43,6 +20,40 @@ import sop.operation.Decrypt; import sop.util.HexUtil; import sop.util.UTCUtil; +import javax.annotation.Nonnull; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.ParseException; +import java.util.Collections; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.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 { private Decrypt decrypt; @@ -74,47 +85,47 @@ public class DecryptCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void missingArgumentsExceptionCausesExit19() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException { when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.MissingArg("Missing arguments.")); - SopCLI.main(new String[] {"decrypt"}); + assertMissingArg(() -> SopCLI.execute("decrypt")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void badDataExceptionCausesExit41() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException { when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"decrypt"}); + assertBadData(() -> SopCLI.execute("decrypt")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.PasswordNotHumanReadable.EXIT_CODE) public void assertNotHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable"); when(decrypt.withPassword(any())).thenThrow(new SOPGPException.PasswordNotHumanReadable()); - SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertPasswordNotHumanReadable(() -> + SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath()) + ); } @Test public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("orange"); - SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertSuccess(() -> SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath())); verify(decrypt, times(1)).withPassword("orange"); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("swordfish"); when(decrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Decrypting with password not supported.")); - SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath()) + ); } @Test public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption { Date now = new Date(); - SopCLI.main(new String[] {"decrypt"}); + assertSuccess(() -> SopCLI.execute("decrypt")); verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME); verify(decrypt, times(1)).verifyNotAfter( ArgumentMatchers.argThat(argument -> { @@ -125,7 +136,8 @@ public class DecryptCmdTest { @Test public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"decrypt", "--not-before", "-", "--not-after", "-"}); + assertSuccess(() -> + SopCLI.execute("decrypt", "--verify-not-before", "-", "--verify-not-after", "-")); verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME); verify(decrypt, times(1)).verifyNotAfter(AbstractSopCmd.END_OF_TIME); } @@ -138,75 +150,88 @@ public class DecryptCmdTest { return Math.abs(now.getTime() - argument.getTime()) <= 1000; }; - SopCLI.main(new String[] {"decrypt", "--not-before", "now", "--not-after", "now"}); + assertSuccess(() -> + SopCLI.execute("decrypt", "--verify-not-before", "now", "--verify-not-after", "now")); verify(decrypt, times(1)).verifyNotAfter(ArgumentMatchers.argThat(isMaxOneSecOff)); verify(decrypt, times(1)).verifyNotBefore(ArgumentMatchers.argThat(isMaxOneSecOff)); } @Test - @ExpectSystemExitWithStatus(1) public void assertMalformedDateInNotBeforeCausesExit1() { // ParserException causes exit(1) - SopCLI.main(new String[] {"decrypt", "--not-before", "invalid"}); + assertGenericError(() -> + SopCLI.execute("decrypt", "--verify-not-before", "invalid")); } @Test - @ExpectSystemExitWithStatus(1) public void assertMalformedDateInNotAfterCausesExit1() { // ParserException causes exit(1) - SopCLI.main(new String[] {"decrypt", "--not-after", "invalid"}); + assertGenericError(() -> + SopCLI.execute("decrypt", "--verify-not-after", "invalid")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertUnsupportedNotAfterCausesExit37() throws SOPGPException.UnsupportedOption { - when(decrypt.verifyNotAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); - SopCLI.main(new String[] {"decrypt", "--not-after", "now"}); + when(decrypt.verifyNotAfter(any())).thenThrow( + new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); + assertUnsupportedOption(() -> + SopCLI.execute("decrypt", "--verify-not-after", "now")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertUnsupportedNotBeforeCausesExit37() throws SOPGPException.UnsupportedOption { - when(decrypt.verifyNotBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); - SopCLI.main(new String[] {"decrypt", "--not-before", "now"}); + when(decrypt.verifyNotBefore(any())).thenThrow( + new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); + assertUnsupportedOption(() -> + SopCLI.execute("decrypt", "--verify-not-before", "now")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE) public void assertExistingSessionKeyOutFileCausesExit59() throws IOException { File tempFile = File.createTempFile("existing-session-key-", ".tmp"); tempFile.deleteOnExit(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); + assertOutputExists(() -> + SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertWhenSessionKeyCannotBeExtractedExit37() throws IOException { Path tempDir = Files.createTempDirectory("session-key-out-dir"); File tempFile = new File(tempDir.toFile(), "session-key"); tempFile.deleteOnExit(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath())); } @Test - public void assertSessionKeyIsProperlyWrittenToSessionKeyFile() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { - byte[] key = "C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137".getBytes(StandardCharsets.UTF_8); + public void assertSessionKeyAndVerificationsIsProperlyWrittenToSessionKeyFile() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException, ParseException { + Date signDate = UTCUtil.parseUTCDate("2022-11-07T15:01:24Z"); + String keyFP = "F9E6F53F7201C60A87064EAB0B27F2B0760A1209"; + String certFP = "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B"; + Verification verification = new Verification(signDate, keyFP, certFP); + SessionKey sessionKey = SessionKey.fromString("9:C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137"); when(decrypt.ciphertext((InputStream) any())).thenReturn(new ReadyWithResult() { @Override public DecryptionResult writeTo(OutputStream outputStream) { return new DecryptionResult( - new SessionKey((byte) 9, key), - Collections.emptyList() + sessionKey, + Collections.singletonList(verification) ); } }); Path tempDir = Files.createTempDirectory("session-key-out-dir"); - File tempFile = new File(tempDir.toFile(), "session-key"); - tempFile.deleteOnExit(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); + File sessionKeyFile = new File(tempDir.toFile(), "session-key"); + sessionKeyFile.deleteOnExit(); + 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())); ByteArrayOutputStream bytesInFile = new ByteArrayOutputStream(); - try (FileInputStream fileIn = new FileInputStream(tempFile)) { + try (FileInputStream fileIn = new FileInputStream(sessionKeyFile)) { byte[] buf = new byte[32]; int read = fileIn.read(buf); while (read != -1) { @@ -215,56 +240,71 @@ public class DecryptCmdTest { } } - byte[] algAndKey = new byte[key.length + 1]; - algAndKey[0] = (byte) 9; - System.arraycopy(key, 0, algAndKey, 1, key.length); - assertArrayEquals(algAndKey, bytesInFile.toByteArray()); + SessionKey parsedSessionKey = SessionKey.fromString(bytesInFile.toString()); + assertEquals(sessionKey, parsedSessionKey); + + bytesInFile = new ByteArrayOutputStream(); + try (FileInputStream fileIn = new FileInputStream(verificationsFile)) { + byte[] buf = new byte[32]; + int read = fileIn.read(buf); + while (read != -1) { + bytesInFile.write(buf, 0, read); + read = fileIn.read(buf); + } + } + + Verification parsedVerification = Verification.fromString(bytesInFile.toString()); + assertEquals(verification, parsedVerification); } @Test - @ExpectSystemExitWithStatus(SOPGPException.CannotDecrypt.EXIT_CODE) public void assertUnableToDecryptExceptionResultsInExit29() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.CannotDecrypt()); - SopCLI.main(new String[] {"decrypt"}); + assertCannotDecrypt(() -> + SopCLI.execute("decrypt")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.NoSignature.EXIT_CODE) - public void assertNoSignatureExceptionCausesExit3() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { + public void assertNoVerificationsIsOkay() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { + File tempFile = File.createTempFile("verify-with-", ".tmp"); + File verifyOut = new File(tempFile.getParent(), "verifications.out"); + verifyOut.deleteOnExit(); when(decrypt.ciphertext((InputStream) any())).thenReturn(new ReadyWithResult() { @Override - public DecryptionResult writeTo(OutputStream outputStream) throws SOPGPException.NoSignature { - throw new SOPGPException.NoSignature(); + public DecryptionResult writeTo(@Nonnull OutputStream outputStream) throws SOPGPException.NoSignature { + return new DecryptionResult(null, Collections.emptyList()); } }); - SopCLI.main(new String[] {"decrypt"}); + assertSuccess(() -> + SopCLI.execute("decrypt", "--verify-with", tempFile.getAbsolutePath(), "--verifications-out", + verifyOut.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void badDataInVerifyWithCausesExit41() throws IOException, SOPGPException.BadData { when(decrypt.verifyWithCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File tempFile = File.createTempFile("verify-with-", ".tmp"); - SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("decrypt", "--verify-with", tempFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void unexistentCertFileCausesExit61() { - SopCLI.main(new String[] {"decrypt", "--verify-with", "invalid"}); + assertMissingInput(() -> + SopCLI.execute("decrypt", "--verify-with", "invalid")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE) public void existingVerifyOutCausesExit59() throws IOException { File certFile = File.createTempFile("existing-verify-out-cert", ".asc"); File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp"); - SopCLI.main(new String[] {"decrypt", "--verify-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); + assertOutputExists(() -> SopCLI.execute("decrypt", "--verifications-out", + existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath())); } @Test - public void verifyOutIsProperlyWritten() throws IOException, SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData { + public void verifyOutIsProperlyWritten() throws IOException, SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, ParseException { File certFile = File.createTempFile("verify-out-cert", ".asc"); File verifyOut = new File(certFile.getParent(), "verify-out.txt"); if (verifyOut.exists()) { @@ -284,7 +324,9 @@ public class DecryptCmdTest { } }); - SopCLI.main(new String[] {"decrypt", "--verify-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("decrypt", "--verifications-out", verifyOut.getAbsolutePath(), + "--verify-with", certFile.getAbsolutePath())); try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) { String line = reader.readLine(); assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line); @@ -299,66 +341,64 @@ public class DecryptCmdTest { File sessionKeyFile1 = TestFileUtil.writeTempStringFile(key1.toString()); File sessionKeyFile2 = TestFileUtil.writeTempStringFile(key2.toString()); - SopCLI.main(new String[] {"decrypt", - "--with-session-key", sessionKeyFile1.getAbsolutePath(), - "--with-session-key", sessionKeyFile2.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("decrypt", + "--with-session-key", sessionKeyFile1.getAbsolutePath(), + "--with-session-key", sessionKeyFile2.getAbsolutePath())); verify(decrypt).withSessionKey(key1); verify(decrypt).withSessionKey(key2); } @Test - @ExpectSystemExitWithStatus(1) public void assertMalformedSessionKeysResultInExit1() throws IOException { File sessionKeyFile = TestFileUtil.writeTempStringFile("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137"); - SopCLI.main(new String[] {"decrypt", - "--with-session-key", sessionKeyFile.getAbsolutePath()}); + assertGenericError(() -> + SopCLI.execute("decrypt", + "--with-session-key", sessionKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void assertBadDataInKeysResultsInExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); + assertBadData(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void assertKeyFileNotFoundCausesExit61() { - SopCLI.main(new String[] {"decrypt", "nonexistent-key"}); + assertMissingInput(() -> SopCLI.execute("decrypt", "nonexistent-key")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE) public void assertProtectedKeyCausesExit67() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); + assertKeyIsProtected(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) public void assertUnsupportedAlgorithmExceptionCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new IOException())); File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); + assertUnsupportedAsymmetricAlgo(() -> + SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void assertMissingPassphraseFileCausesExit61() { - SopCLI.main(new String[] {"decrypt", "--with-password", "missing"}); + assertMissingInput(() -> + SopCLI.execute("decrypt", "--with-password", "missing")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void assertMissingSessionKeyFileCausesExit61() { - SopCLI.main(new String[] {"decrypt", "--with-session-key", "missing"}); + assertMissingInput(() -> + SopCLI.execute("decrypt", "--with-session-key", "missing")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.IncompleteVerification.EXIT_CODE) public void verifyOutWithoutVerifyWithCausesExit23() { - SopCLI.main(new String[] {"decrypt", "--verify-out", "out.file"}); + assertIncompleteVerification(() -> + SopCLI.execute("decrypt", "--verifications-out", "out.file")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java index 73ec9cb..85ae052 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java @@ -4,28 +4,40 @@ 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 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; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertCertCannotEncrypt; +import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; +import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyCannotSign; +import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyIsProtected; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput; +import static sop.testsuite.assertions.SopExecutionAssertions.assertPasswordNotHumanReadable; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedAsymmetricAlgo; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption; public class EncryptCmdTest { @@ -34,10 +46,10 @@ public class EncryptCmdTest { @BeforeEach public void mockComponents() throws IOException { encrypt = mock(Encrypt.class); - when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() { + when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult() { @Override - public void writeTo(OutputStream outputStream) { - + public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException { + return new EncryptionResult(null); } }); @@ -48,48 +60,50 @@ public class EncryptCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) - public void missingBothPasswordAndCertFileCauseExit19() { - SopCLI.main(new String[] {"encrypt", "--no-armor"}); + public void missingBothPasswordAndCertFileCausesMissingArg() { + assertMissingArg(() -> + SopCLI.execute("encrypt", "--no-armor")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void as_unsupportedEncryptAsCausesExit37() throws SOPGPException.UnsupportedOption { + public void as_unsupportedEncryptAsCausesUnsupportedOption() throws SOPGPException.UnsupportedOption { when(encrypt.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting encryption mode not supported.")); - SopCLI.main(new String[] {"encrypt", "--as", "Binary"}); + assertUnsupportedOption(() -> + SopCLI.execute("encrypt", "--as", "Binary")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void as_invalidModeOptionCausesExit37() { - SopCLI.main(new String[] {"encrypt", "--as", "invalid"}); + public void as_invalidModeOptionCausesUnsupportedOption() { + assertUnsupportedOption(() -> + SopCLI.execute("encrypt", "--as", "invalid")); } @Test public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("0rbit"); for (EncryptAs mode : EncryptAs.values()) { - SopCLI.main(new String[] {"encrypt", "--as", mode.name(), "--with-password", passwordFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("encrypt", "--as", mode.name(), + "--with-password", passwordFile.getAbsolutePath())); verify(encrypt, times(1)).mode(mode); } } @Test - @ExpectSystemExitWithStatus(SOPGPException.PasswordNotHumanReadable.EXIT_CODE) - public void withPassword_notHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { + public void withPassword_notHumanReadablePasswordCausesPWNotHumanReadable() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable()); File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertPasswordNotHumanReadable(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void withPassword_unsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { + public void withPassword_unsupportedWithPasswordCausesUnsupportedOption() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported.")); File passwordFile = TestFileUtil.writeTempStringFile("orange"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); } @Test @@ -97,106 +111,115 @@ public class EncryptCmdTest { File keyFile1 = File.createTempFile("sign-with-1-", ".asc"); File keyFile2 = File.createTempFile("sign-with-2-", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("password"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile1.getAbsolutePath(), "--sign-with", keyFile2.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), + "--sign-with", keyFile1.getAbsolutePath(), + "--sign-with", keyFile2.getAbsolutePath())); verify(encrypt, times(2)).signWith((InputStream) any()); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) - public void signWith_nonExistentKeyFileCausesExit61() { - SopCLI.main(new String[] {"encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"}); + public void signWith_nonExistentKeyFileCausesMissingInput() { + assertMissingInput(() -> + SopCLI.execute("encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE) - public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + public void signWith_keyIsProtectedCausesKeyIsProtected() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); File keyFile = File.createTempFile("sign-with", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("starship"); - SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", passwordFile.getAbsolutePath()}); + assertKeyIsProtected(() -> + SopCLI.execute("encrypt", "--sign-with", keyFile.getAbsolutePath(), + "--with-password", passwordFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) - public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + public void signWith_unsupportedAsymmetricAlgoCausesUnsupportedAsymAlgo() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); File keyFile = File.createTempFile("sign-with", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("123456"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); + assertUnsupportedAsymmetricAlgo(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), + "--sign-with", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyCannotSign.EXIT_CODE) - public void signWith_certCannotSignCausesExit79() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { + public void signWith_certCannotSignCausesKeyCannotSign() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign()); File keyFile = File.createTempFile("sign-with", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("dragon"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); + assertKeyCannotSign(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), + "--sign-with", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) - public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + public void signWith_badDataCausesBadData() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File keyFile = File.createTempFile("sign-with", ".asc"); File passwordFile = TestFileUtil.writeTempStringFile("orange"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), + "--sign-with", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) - public void cert_nonExistentCertFileCausesExit61() { - SopCLI.main(new String[] {"encrypt", "invalid.asc"}); + public void cert_nonExistentCertFileCausesMissingInput() { + assertMissingInput(() -> + SopCLI.execute("encrypt", "invalid.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) - public void cert_unsupportedAsymmetricAlgorithmCausesExit13() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { + public void cert_unsupportedAsymmetricAlgorithmCausesUnsupportedAsymAlg() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); + assertUnsupportedAsymmetricAlgo(() -> + SopCLI.execute("encrypt", certFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.CertCannotEncrypt.EXIT_CODE) - public void cert_certCannotEncryptCausesExit17() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { + public void cert_certCannotEncryptCausesCertCannotEncrypt() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.CertCannotEncrypt("Certificate cannot encrypt.", new Exception())); File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); + assertCertCannotEncrypt(() -> + SopCLI.execute("encrypt", certFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) - public void cert_badDataCausesExit41() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { + public void cert_badDataCausesBadData() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("encrypt", certFile.getAbsolutePath())); } @Test public void noArmor_notCalledByDefault() throws IOException { File passwordFile = TestFileUtil.writeTempStringFile("clownfish"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); verify(encrypt, never()).noArmor(); } @Test public void noArmor_callGetsPassedDown() throws IOException { File passwordFile = TestFileUtil.writeTempStringFile("monkey"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--no-armor"}); + assertSuccess(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), "--no-armor")); verify(encrypt, times(1)).noArmor(); } @Test - @ExpectSystemExitWithStatus(1) - public void writeTo_ioExceptionCausesExit1() throws IOException { - when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() { + public void writeTo_ioExceptionCausesGenericError() throws IOException { + when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException { throw new IOException(); } }); File passwordFile = TestFileUtil.writeTempStringFile("wildcat"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertGenericError(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java index 12f837d..3b046a0 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java @@ -10,12 +10,14 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.Ready; @@ -45,32 +47,34 @@ public class ExtractCertCmdTest { @Test public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"extract-cert"}); + assertSuccess(() -> + SopCLI.execute("extract-cert")); verify(extractCert, never()).noArmor(); } @Test public void noArmor_passedDown() { - SopCLI.main(new String[] {"extract-cert", "--no-armor"}); + assertSuccess(() -> + SopCLI.execute("extract-cert", "--no-armor")); verify(extractCert, times(1)).noArmor(); } @Test - @ExpectSystemExitWithStatus(1) - public void key_ioExceptionCausesExit1() throws IOException, SOPGPException.BadData { + public void key_ioExceptionCausesGenericError() throws IOException, SOPGPException.BadData { when(extractCert.key((InputStream) any())).thenReturn(new Ready() { @Override public void writeTo(OutputStream outputStream) throws IOException { throw new IOException(); } }); - SopCLI.main(new String[] {"extract-cert"}); + assertGenericError(() -> + SopCLI.execute("extract-cert")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) - public void key_badDataCausesExit41() throws IOException, SOPGPException.BadData { + public void key_badDataCausesBadData() throws IOException, SOPGPException.BadData { when(extractCert.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"extract-cert"}); + assertBadData(() -> + SopCLI.execute("extract-cert")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java index e7ebf1a..126c851 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java @@ -10,11 +10,14 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; +import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedAsymmetricAlgo; import java.io.IOException; import java.io.OutputStream; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InOrder; @@ -47,19 +50,22 @@ public class GenerateKeyCmdTest { @Test public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"generate-key", "Alice"}); + assertSuccess(() -> + SopCLI.execute("generate-key", "Alice")); verify(generateKey, never()).noArmor(); } @Test public void noArmor_passedDown() { - SopCLI.main(new String[] {"generate-key", "--no-armor", "Alice"}); + assertSuccess(() -> + SopCLI.execute("generate-key", "--no-armor", "Alice")); verify(generateKey, times(1)).noArmor(); } @Test public void userId_multipleUserIdsPassedDownInProperOrder() { - SopCLI.main(new String[] {"generate-key", "Alice ", "Bob "}); + assertSuccess(() -> + SopCLI.execute("generate-key", "Alice ", "Bob ")); InOrder inOrder = Mockito.inOrder(generateKey); inOrder.verify(generateKey).userId("Alice "); @@ -69,30 +75,32 @@ public class GenerateKeyCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void missingArgumentCausesExit19() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { // TODO: RFC4880-bis and the current Stateless OpenPGP CLI spec allow keys to have no user-ids, // so we might want to change this test in the future. when(generateKey.generate()).thenThrow(new SOPGPException.MissingArg("Missing user-id.")); - SopCLI.main(new String[] {"generate-key"}); + assertMissingArg(() -> + SopCLI.execute("generate-key")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) public void unsupportedAsymmetricAlgorithmCausesExit13() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { when(generateKey.generate()).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); - SopCLI.main(new String[] {"generate-key", "Alice"}); + assertUnsupportedAsymmetricAlgo(() -> + SopCLI.execute("generate-key", "Alice")); + } @Test - @ExpectSystemExitWithStatus(1) - public void ioExceptionCausesExit1() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { + public void ioExceptionCausesGenericError() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { when(generateKey.generate()).thenReturn(new Ready() { @Override public void writeTo(OutputStream outputStream) throws IOException { throw new IOException(); } }); - SopCLI.main(new String[] {"generate-key", "Alice"}); + + assertGenericError(() -> + SopCLI.execute("generate-key", "Alice")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java new file mode 100644 index 0000000..a230aaa --- /dev/null +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import sop.ReadyWithResult; +import sop.SOP; +import sop.Signatures; +import sop.cli.picocli.SopCLI; +import sop.cli.picocli.TestFileUtil; +import sop.exception.SOPGPException; +import sop.operation.InlineDetach; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +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.assertMissingArg; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; + +public class InlineDetachCmdTest { + + InlineDetach inlineDetach; + + @BeforeEach + public void mockComponents() { + inlineDetach = mock(InlineDetach.class); + + SOP sop = mock(SOP.class); + when(sop.inlineDetach()).thenReturn(inlineDetach); + SopCLI.setSopInstance(sop); + } + + @Test + public void testMissingSignaturesOutResultsInMissingArg() { + assertMissingArg(() -> + SopCLI.execute("inline-detach")); + } + + @Test + public void testNoArmorIsCalled() throws IOException { + // Create temp dir and allocate non-existing tempfile for sigout + File tempDir = TestFileUtil.createTempDir(); + File tempFile = new File(tempDir, "sigs.out"); + tempFile.deleteOnExit(); + + // mock inline-detach + when(inlineDetach.message((InputStream) any())) + .thenReturn(new ReadyWithResult() { + @Override + public Signatures writeTo(OutputStream outputStream) throws SOPGPException.NoSignature { + return new Signatures() { + @Override + public void writeTo(OutputStream signatureOutputStream) throws IOException { + signatureOutputStream.write("Signatures!\n".getBytes(StandardCharsets.UTF_8)); + } + }; + } + }); + + assertSuccess(() -> + SopCLI.execute("inline-detach", "--signatures-out", tempFile.getAbsolutePath(), "--no-armor")); + verify(inlineDetach, times(1)).noArmor(); + verify(inlineDetach, times(1)).message((InputStream) any()); + } +} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java index 3b96012..324d39a 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java @@ -10,19 +10,27 @@ 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; import sop.SOP; import sop.SigningResult; import sop.cli.picocli.SopCLI; +import sop.cli.picocli.TestFileUtil; import sop.exception.SOPGPException; import sop.operation.DetachedSign; @@ -30,6 +38,7 @@ public class SignCmdTest { DetachedSign detachedSign; File keyFile; + File passFile; @BeforeEach public void mockComponents() throws IOException, SOPGPException.ExpectedText { @@ -47,68 +56,82 @@ public class SignCmdTest { SopCLI.setSopInstance(sop); keyFile = File.createTempFile("sign-", ".asc"); + passFile = TestFileUtil.writeTempStringFile("sw0rdf1sh"); } @Test public void as_optionsAreCaseInsensitive() { - SopCLI.main(new String[] {"sign", "--as", "Binary", keyFile.getAbsolutePath()}); - SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); - SopCLI.main(new String[] {"sign", "--as", "BINARY", keyFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("sign", "--as", "Binary", keyFile.getAbsolutePath())); + assertSuccess(() -> + SopCLI.execute("sign", "--as", "binary", keyFile.getAbsolutePath())); + assertSuccess(() -> + SopCLI.execute("sign", "--as", "BINARY", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void as_invalidOptionCausesExit37() { - SopCLI.main(new String[] {"sign", "--as", "Invalid", keyFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("sign", "--as", "Invalid", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { when(detachedSign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting signing mode not supported.")); - SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("sign", "--as", "binary", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void key_nonExistentKeyFileCausesExit61() { - SopCLI.main(new String[] {"sign", "invalid.asc"}); + assertMissingInput(() -> + SopCLI.execute("sign", "invalid.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE) public void key_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertKeyIsProtected(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void key_missingKeyFileCausesExit19() { - SopCLI.main(new String[] {"sign"}); + assertMissingArg(() -> + SopCLI.execute("sign")); } @Test public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); verify(detachedSign, never()).noArmor(); } @Test public void noArmor_passedDown() { - SopCLI.main(new String[] {"sign", "--no-armor", keyFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("sign", "--no-armor", keyFile.getAbsolutePath())); verify(detachedSign, times(1)).noArmor(); } @Test - @ExpectSystemExitWithStatus(1) + public void withKeyPassword_passedDown() { + assertSuccess(() -> + SopCLI.execute("sign", + "--with-key-password", passFile.getAbsolutePath(), + keyFile.getAbsolutePath())); + verify(detachedSign, times(1)).withKeyPassword("sw0rdf1sh"); + } + + @Test public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText { when(detachedSign.data((InputStream) any())).thenReturn(new ReadyWithResult() { @Override @@ -116,13 +139,14 @@ public class SignCmdTest { throw new IOException(); } }); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertGenericError(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.ExpectedText.EXIT_CODE) public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText { when(detachedSign.data((InputStream) any())).thenThrow(new SOPGPException.ExpectedText()); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertExpectedText(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/TestEnvironmentVariableResolver.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/TestEnvironmentVariableResolver.java new file mode 100644 index 0000000..6665ada --- /dev/null +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/TestEnvironmentVariableResolver.java @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands; + +import java.util.HashMap; +import java.util.Map; + +public class TestEnvironmentVariableResolver implements AbstractSopCmd.EnvironmentVariableResolver { + + private final Map environment = new HashMap<>(); + + public void addEnvironmentVariable(String name, String value) { + this.environment.put(name, value); + } + + @Override + public String resolveEnvironmentVariable(String name) { + return environment.get(name); + } +} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java index c33cf74..3c9724f 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java @@ -10,17 +10,22 @@ 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; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; +import java.text.ParseException; import java.util.Arrays; import java.util.Collections; import java.util.Date; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -41,7 +46,7 @@ public class VerifyCmdTest { PrintStream originalSout; @BeforeEach - public void prepare() throws SOPGPException.UnsupportedOption, SOPGPException.BadData, SOPGPException.NoSignature, IOException { + public void prepare() throws SOPGPException.UnsupportedOption, SOPGPException.BadData, SOPGPException.NoSignature, IOException, ParseException { originalSout = System.out; detachedVerify = mock(DetachedVerify.class); @@ -73,62 +78,77 @@ public class VerifyCmdTest { } @Test - public void notAfter_passedDown() throws SOPGPException.UnsupportedOption { + public void notAfter_passedDown() throws SOPGPException.UnsupportedOption, ParseException { Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"); - SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-after", "2019-10-29T18:36:45Z", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notAfter(date); } @Test public void notAfter_now() throws SOPGPException.UnsupportedOption { Date now = new Date(); - SopCLI.main(new String[] {"verify", "--not-after", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-after", "now", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notAfter(dateMatcher(now)); } @Test public void notAfter_dashCountsAsEndOfTime() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"verify", "--not-after", "-", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-after", "-", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notAfter(AbstractSopCmd.END_OF_TIME); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { when(detachedVerify.notAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); - SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("verify", "--not-after", "2019-10-29T18:36:45Z", + signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test - public void notBefore_passedDown() throws SOPGPException.UnsupportedOption { + public void notBefore_passedDown() throws SOPGPException.UnsupportedOption, ParseException { Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"); - SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-before", "2019-10-29T18:36:45Z", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notBefore(date); } @Test public void notBefore_now() throws SOPGPException.UnsupportedOption { Date now = new Date(); - SopCLI.main(new String[] {"verify", "--not-before", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-before", "now", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notBefore(dateMatcher(now)); } @Test public void notBefore_dashCountsAsBeginningOfTime() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"verify", "--not-before", "-", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", "--not-before", "-", + signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { when(detachedVerify.notBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); - SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("verify", "--not-before", "2019-10-29T18:36:45Z", + signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test public void notBeforeAndNotAfterAreCalledWithDefaultValues() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); verify(detachedVerify, times(1)).notAfter(dateMatcher(new Date())); verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME); } @@ -138,47 +158,47 @@ public class VerifyCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void cert_fileNotFoundCausesExit61() { - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), "invalid.asc"}); + assertMissingInput(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), "invalid.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void cert_badDataCausesExit41() throws SOPGPException.BadData, IOException { when(detachedVerify.cert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void signature_fileNotFoundCausesExit61() { - SopCLI.main(new String[] {"verify", "invalid.sig", cert.getAbsolutePath()}); + assertMissingInput(() -> + SopCLI.execute("verify", "invalid.sig", cert.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void signature_badDataCausesExit41() throws SOPGPException.BadData, IOException { when(detachedVerify.signatures((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.NoSignature.EXIT_CODE) public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.NoSignature()); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertNoSignature(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); } @Test - public void resultIsPrintedProperly() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { + public void resultIsPrintedProperly() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData, ParseException { when(detachedVerify.data((InputStream) any())).thenReturn(Arrays.asList( new Verification(UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"), "EB85BB5FA33A75E15E944E63F231550C4F47E38E", @@ -191,7 +211,8 @@ public class VerifyCmdTest { ByteArrayOutputStream out = new ByteArrayOutputStream(); System.setOut(new PrintStream(out)); - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath())); System.setOut(originalSout); diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java index e284e35..92850bd 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java @@ -4,19 +4,19 @@ package sop.cli.picocli.commands; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.SOP; import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; import sop.operation.Version; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; +import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption; + public class VersionCmdTest { private Version version; @@ -29,6 +29,8 @@ public class VersionCmdTest { when(version.getVersion()).thenReturn("1.0"); when(version.getExtendedVersion()).thenReturn("MockSop Extended Version Information"); when(version.getBackendVersion()).thenReturn("Foo"); + when(version.getSopSpecVersion()).thenReturn("draft-dkg-openpgp-stateless-cli-XX"); + when(version.getSopVVersion()).thenReturn("1.0"); when(sop.version()).thenReturn(version); SopCLI.setSopInstance(sop); @@ -36,26 +38,41 @@ public class VersionCmdTest { @Test public void assertVersionCommandWorks() { - SopCLI.main(new String[] {"version"}); + assertSuccess(() -> + SopCLI.execute("version")); verify(version, times(1)).getVersion(); verify(version, times(1)).getName(); } @Test public void assertExtendedVersionCommandWorks() { - SopCLI.main(new String[] {"version", "--extended"}); + assertSuccess(() -> + SopCLI.execute("version", "--extended")); verify(version, times(1)).getExtendedVersion(); } @Test public void assertBackendVersionCommandWorks() { - SopCLI.main(new String[] {"version", "--backend"}); + assertSuccess(() -> + SopCLI.execute("version", "--backend")); verify(version, times(1)).getBackendVersion(); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) + public void assertSpecVersionCommandWorks() { + assertSuccess(() -> + SopCLI.execute("version", "--sop-spec")); + } + + @Test + public void assertSOPVVersionCommandWorks() { + assertSuccess(() -> + SopCLI.execute("version", "--sopv")); + } + + @Test public void assertInvalidOptionResultsInExit37() { - SopCLI.main(new String[] {"version", "--invalid"}); + assertUnsupportedOption(() -> + SopCLI.execute("version", "--invalid")); } } diff --git a/sop-java-testfixtures/build.gradle b/sop-java-testfixtures/build.gradle new file mode 100644 index 0000000..d3d4a1e --- /dev/null +++ b/sop-java-testfixtures/build.gradle @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +plugins { + id 'java-library' +} + +group 'org.pgpainless' + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":sop-java")) + implementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + implementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" + runtimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + // @Nullable, @Nonnull annotations + implementation "com.google.code.findbugs:jsr305:3.0.2" + +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOption.java b/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOption.java new file mode 100644 index 0000000..cbd0746 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOption.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface AbortOnUnsupportedOption { + +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOptionExtension.java b/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOptionExtension.java new file mode 100644 index 0000000..0bf366d --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOptionExtension.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; +import sop.exception.SOPGPException; + +import java.lang.annotation.Annotation; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +public class AbortOnUnsupportedOptionExtension implements TestExecutionExceptionHandler { + + @Override + public void handleTestExecutionException(ExtensionContext extensionContext, Throwable throwable) throws Throwable { + Class testClass = extensionContext.getRequiredTestClass(); + Annotation annotation = testClass.getAnnotation(AbortOnUnsupportedOption.class); + if (annotation != null && throwable instanceof SOPGPException.UnsupportedOption) { + assumeTrue(false, "Test aborted due to: " + throwable.getMessage()); + } + throw throwable; + } +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/JUtils.java b/sop-java-testfixtures/src/main/java/sop/testsuite/JUtils.java new file mode 100644 index 0000000..5302192 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/JUtils.java @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite; + +import sop.util.UTCUtil; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class JUtils { + + /** + * Return true, if the given
array
starts with
start
. + * + * @param array array + * @param start start + * @return true if array starts with start, false otherwise + */ + public static boolean arrayStartsWith(byte[] array, byte[] start) { + return arrayStartsWith(array, start, 0); + } + + /** + * Return true, if the given
array
contains the given
start
at offset
offset
. + * + * @param array array + * @param start start + * @param offset offset + * @return true, if array contains start at offset, false otherwise + */ + public static boolean arrayStartsWith(byte[] array, byte[] start, int offset) { + if (offset < 0) { + throw new IllegalArgumentException("Offset cannot be negative"); + } + + if (start.length + offset > array.length) { + return false; + } + + for (int i = 0; i < start.length; i++) { + if (array[offset + i] != start[i]) { + return false; + } + } + return true; + } + + /** + * Assert that the given
array
starts with
start
. + * + * @param array array + * @param start start + */ + public static void assertArrayStartsWith(byte[] array, byte[] start) { + if (!arrayStartsWith(array, start)) { + byte[] actual = new byte[Math.min(start.length, array.length)]; + System.arraycopy(array, 0, actual, 0, actual.length); + fail("Array does not start with expected bytes.\n" + + "Expected: <" + Arrays.toString(start) + ">\n" + + "Actual: <" + Arrays.toString(actual) + ">"); + } + } + + /** + * Assert that the given
array
contains
start
at
offset
. + * + * @param array array + * @param start start + * @param offset offset + */ + public static void assertArrayStartsWith(byte[] array, byte[] start, int offset) { + if (!arrayStartsWith(array, start, offset)) { + byte[] actual = new byte[Math.min(start.length, array.length - offset)]; + System.arraycopy(array, offset, actual, 0, actual.length); + fail("Array does not start with expected bytes at offset " + offset + ".\n" + + "Expected: <" + Arrays.toString(start) + ">\n" + + "Actual: <" + Arrays.toString(actual) + ">"); + } + } + + public static boolean arrayEndsWith(byte[] array, byte[] end) { + return arrayEndsWith(array, end, 0); + } + + public static boolean arrayEndsWith(byte[] array, byte[] end, int offset) { + if (end.length + offset > array.length) { + return false; + } + + for (int i = 0; i < end.length; i++) { + int arrOff = array.length - end.length - offset; + if (end[i] != array[arrOff + i]) { + return false; + } + } + return true; + } + + public static void assertArrayEndsWith(byte[] array, byte[] end) { + assertArrayEndsWith(array, end, 0); + } + + public static void assertArrayEndsWith(byte[] array, byte[] end, int offset) { + if (!arrayEndsWith(array, end, offset)) { + byte[] actual = new byte[Math.min(end.length, array.length - offset)]; + System.arraycopy(array, array.length - actual.length, actual, 0, actual.length); + fail("Array does not end with the expected bytes.\n" + + "Expected: <" + Arrays.toString(end) + ">\n" + + "Actual: <" + Arrays.toString(actual) + ">"); + } + } + + public static void assertArrayEndsWithIgnoreNewlines(byte[] array, byte[] end) { + int offset = 0; + while (offset < array.length && array[array.length - 1 - offset] == (byte) 10) { + offset++; + } + + assertArrayEndsWith(array, end, offset); + } + + /** + * Assert equality of the given two ascii armored byte arrays, ignoring armor header lines. + * + * @param first first ascii armored bytes + * @param second second ascii armored bytes + */ + public static void assertAsciiArmorEquals(byte[] first, byte[] second) { + byte[] firstCleaned = removeArmorHeaders(first); + byte[] secondCleaned = removeArmorHeaders(second); + + assertArrayEquals(firstCleaned, secondCleaned); + } + + /** + * Remove armor headers "Comment:", "Version:", "MessageID:", "Hash:" and "Charset:" along with their values + * from the given ascii armored byte array. + * + * @param armor ascii armored byte array + * @return ascii armored byte array with header lines removed + */ + public static byte[] removeArmorHeaders(byte[] armor) { + String string = new String(armor, StandardCharsets.UTF_8); + string = string.replaceAll("Comment: .+\\R", "") + .replaceAll("Version: .+\\R", "") + .replaceAll("MessageID: .+\\R", "") + .replaceAll("Hash: .+\\R", "") + .replaceAll("Charset: .+\\R", ""); + return string.getBytes(StandardCharsets.UTF_8); + } + + public static void assertDateEquals(Date expected, Date actual) { + assertEquals(UTCUtil.formatUTCDate(expected), UTCUtil.formatUTCDate(actual)); + } + + public static boolean dateEquals(Date expected, Date actual) { + return UTCUtil.formatUTCDate(expected).equals(UTCUtil.formatUTCDate(actual)); + } + +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/SOPInstanceFactory.java b/sop-java-testfixtures/src/main/java/sop/testsuite/SOPInstanceFactory.java new file mode 100644 index 0000000..106c494 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/SOPInstanceFactory.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite; + +import sop.SOP; + +import java.util.Map; + +/** + * Factory class to instantiate SOP implementations for testing. + * Overwrite this class and the {@link #provideSOPInstances()} method to return the SOP instances you want + * to test. + * Then, add the following line to your
build.gradle
files
dependencies
section: + *
{@code
+ *     testImplementation(testFixtures("org.pgpainless:sop-java:"))
+ * }
+ * To inject the factory class into the test suite, add the following line to your modules
test
task: + *
{@code
+ *     environment("test.implementation", "org.example.YourTestSubjectFactory")
+ * }
+ * Next, in your
test
sources, extend all test classes from the
testFixtures
+ *
sop.operation
package. + * Take a look at the
external-sop
module for an example. + */ +public abstract class SOPInstanceFactory { + + public abstract Map provideSOPInstances(); +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/TestData.java b/sop-java-testfixtures/src/main/java/sop/testsuite/TestData.java new file mode 100644 index 0000000..386c411 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/TestData.java @@ -0,0 +1,433 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite; + +import sop.util.UTCUtil; + +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.Date; + +public class TestData { + + + public static final String PLAINTEXT = "Hello, World!\n"; + + // 'Alice' key from draft-bre-openpgp-samples-00 + public static final String ALICE_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Comment: EB85 BB5F A33A 75E1 5E94 4E63 F231 550C 4F47 E38E\n" + + "Comment: Alice Lovelace \n" + + "\n" + + "xjMEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U\n" + + "b7O1u13NJkFsaWNlIExvdmVsYWNlIDxhbGljZUBvcGVucGdwLmV4YW1wbGU+wpAE\n" + + "ExYIADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQTrhbtfozp14V6UTmPy\n" + + "MVUMT0fjjgUCXaWfOgAKCRDyMVUMT0fjjukrAPoDnHBSogOmsHOsd9qGsiZpgRnO\n" + + "dypvbm+QtXZqth9rvwD9HcDC0tC+PHAsO7OTh1S1TC9RiJsvawAfCPaQZoed8gLO\n" + + "OARcRwTpEgorBgEEAZdVAQUBAQdAQv8GIa2rSTzgqbXCpDDYMiKRVitCsy203x3s\n" + + "E9+eviIDAQgHwngEGBYIACAWIQTrhbtfozp14V6UTmPyMVUMT0fjjgUCXEcE6QIb\n" + + "DAAKCRDyMVUMT0fjjlnQAQDFHUs6TIcxrNTtEZFjUFm1M0PJ1Dng/cDW4xN80fsn\n" + + "0QEA22Kr7VkCjeAEC08VSTeV+QFsmz55/lntWkwYWhmvOgE=\n" + + "=QX3Q\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + public static final String ALICE_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: EB85 BB5F A33A 75E1 5E94 4E63 F231 550C 4F47 E38E\n" + + "Comment: Alice Lovelace \n" + + "\n" + + "xVgEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U\n" + + "b7O1u10AAP9XBeW6lzGOLx7zHH9AsUDUTb2pggYGMzd0P3ulJ2AfvQ4RzSZBbGlj\n" + + "ZSBMb3ZlbGFjZSA8YWxpY2VAb3BlbnBncC5leGFtcGxlPsKQBBMWCAA4AhsDBQsJ\n" + + "CAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE64W7X6M6deFelE5j8jFVDE9H444FAl2l\n" + + "nzoACgkQ8jFVDE9H447pKwD6A5xwUqIDprBzrHfahrImaYEZzncqb25vkLV2arYf\n" + + "a78A/R3AwtLQvjxwLDuzk4dUtUwvUYibL2sAHwj2kGaHnfICx10EXEcE6RIKKwYB\n" + + "BAGXVQEFAQEHQEL/BiGtq0k84Km1wqQw2DIikVYrQrMttN8d7BPfnr4iAwEIBwAA\n" + + "/3/xFPG6U17rhTuq+07gmEvaFYKfxRB6sgAYiW6TMTpQEK7CeAQYFggAIBYhBOuF\n" + + "u1+jOnXhXpROY/IxVQxPR+OOBQJcRwTpAhsMAAoJEPIxVQxPR+OOWdABAMUdSzpM\n" + + "hzGs1O0RkWNQWbUzQ8nUOeD9wNbjE3zR+yfRAQDbYqvtWQKN4AQLTxVJN5X5AWyb\n" + + "Pnn+We1aTBhaGa86AQ==\n" + + "=3GfK\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + public static final String ALICE_PRIMARY_FINGERPRINT = "EB85BB5FA33A75E15E944E63F231550C4F47E38E"; + public static final String ALICE_SIGNING_FINGERPRINT = "EB85BB5FA33A75E15E944E63F231550C4F47E38E"; + + public static final String ALICE_INLINE_SIGNED_MESSAGE = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "owGbwMvMwCX2yTCUx9/9cR/jaZEkBhDwSM3JyddRCM8vyklR5OooZWEQ42JQZ2VK\n" + + "PjjpPacATLmYIsvr1t3xi61KH8ZN8UuGCTMwpPcw/E9jS+vcvPu2gmp4jcRbcSNP\n" + + "FYmW8hmLJdUVrdt1V8w6GM/IMEvN0tP339sNGX4swq8T5p62q3jUfLjpstmcI6Ie\n" + + "sfcfswMA\n" + + "=RDAo\n" + + "-----END PGP MESSAGE-----"; + public static final Date ALICE_INLINE_SIGNED_MESSAGE_DATE = parseUTCDate("2023-01-13T17:20:47Z"); + // signature over PLAINTEXT + public static final String ALICE_DETACHED_SIGNED_MESSAGE = "-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "iHUEABYKACcFAmPBjZUJEPIxVQxPR+OOFiEE64W7X6M6deFelE5j8jFVDE9H444A\n" + + "ADI/AQC6Bux6WpGYf7HO+QPV/D5iIrqZt9xPLgfUVoNJBmMZZwD+Ib+tn5pSyWUw\n" + + "0K1UgT5roym9Fln8U5W8R03TSbfNiwE=\n" + + "=bxPN\n" + + "-----END PGP SIGNATURE-----"; + public static final Date ALICE_DETACHED_SIGNED_MESSAGE_DATE = parseUTCDate("2023-01-13T16:57:57Z"); + + // 'Bob' key from draft-bre-openpgp-samples-00 + public static final String BOB_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Comment: D1A6 6E1A 23B1 82C9 980F 788C FBFC C82A 015E 7330\n" + + "Comment: Bob Babbage \n" + + "\n" + + "xsDNBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + + "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + + "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + + "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + + "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + + "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + + "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + + "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + + "vLIwa3T4CyshfT0AEQEAAc0hQm9iIEJhYmJhZ2UgPGJvYkBvcGVucGdwLmV4YW1w\n" + + "bGU+wsEOBBMBCgA4AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAFiEE0aZuGiOx\n" + + "gsmYD3iM+/zIKgFeczAFAl2lnvoACgkQ+/zIKgFeczBvbAv/VNk90a6hG8Od9xTz\n" + + "XxH5YRFUSGfIA1yjPIVOnKqhMwps2U+sWE3urL+MvjyQRlyRV8oY9IOhQ5Esm6DO\n" + + "ZYrTnE7qVETm1ajIAP2OFChEc55uH88x/anpPOXOJY7S8jbn3naC9qad75BrZ+3g\n" + + "9EBUWiy5p8TykP05WSnSxNRt7vFKLfEB4nGkehpwHXOVF0CRNwYle42bg8lpmdXF\n" + + "DcCZCi+qEbafmTQzkAqyzS3nCh3IAqq6Y0kBuaKLm2tSNUOlZbD+OHYQNZ5Jix7c\n" + + "ZUzs6Xh4+I55NRWl5smrLq66yOQoFPy9jot/Qxikx/wP3MsAzeGaZSEPc0fHp5G1\n" + + "6rlGbxQ3vl8/usUV7W+TMEMljgwd5x8POR6HC8EaCDfVnUBCPi/Gv+egLjsIbPJZ\n" + + "ZEroiE40e6/UoCiQtlpQB5exPJYSd1Q1txCwueih99PHepsDhmUQKiACszNU+RRo\n" + + "zAYau2VdHqnRJ7QYdxHDiH49jPK4NTMyb/tJh2TiIwcmsIpGzsDNBF2lnPIBDADW\n" + + "ML9cbGMrp12CtF9b2P6z9TTT74S8iyBOzaSvdGDQY/sUtZXRg21HWamXnn9sSXvI\n" + + "DEINOQ6A9QxdxoqWdCHrOuW3ofneYXoG+zeKc4dC86wa1TR2q9vW+RMXSO4uImA+\n" + + "Uzula/6k1DogDf28qhCxMwG/i/m9g1c/0aApuDyKdQ1PXsHHNlgd/Dn6rrd5y2AO\n" + + "baifV7wIhEJnvqgFXDN2RXGjLeCOHV4Q2WTYPg/S4k1nMXVDwZXrvIsA0YwIMgIT\n" + + "86Rafp1qKlgPNbiIlC1g9RY/iFaGN2b4Ir6GDohBQSfZW2+LXoPZuVE/wGlQ01rh\n" + + "827KVZW4lXvqsge+wtnWlszcselGATyzqOK9LdHPdZGzROZYI2e8c+paLNDdVPL6\n" + + "vdRBUnkCaEkOtl1mr2JpQi5nTU+gTX4IeInC7E+1a9UDF/Y85ybUz8XV8rUnR76U\n" + + "qVC7KidNepdHbZjjXCt8/Zo+Tec9JNbYNQB/e9ExmDntmlHEsSEQzFwzj8sxH48A\n" + + "EQEAAcLA9gQYAQoAIBYhBNGmbhojsYLJmA94jPv8yCoBXnMwBQJdpZzyAhsMAAoJ\n" + + "EPv8yCoBXnMw6f8L/26C34dkjBffTzMj5Bdzm8MtF67OYneJ4TQMw7+41IL4rVcS\n" + + "KhIhk/3Ud5knaRtP2ef1+5F66h9/RPQOJ5+tvBwhBAcUWSupKnUrdVaZQanYmtSx\n" + + "cVV2PL9+QEiNN3tzluhaWO//rACxJ+K/ZXQlIzwQVTpNhfGzAaMVV9zpf3u0k14i\n" + + "tcv6alKY8+rLZvO1wIIeRZLmU0tZDD5HtWDvUV7rIFI1WuoLb+KZgbYn3OWjCPHV\n" + + "dTrdZ2CqnZbG3SXw6awH9bzRLV9EXkbhIMez0deCVdeo+wFFklh8/5VK2b0vk/+w\n" + + "qMJxfpa1lHvJLobzOP9fvrswsr92MA2+k901WeISR7qEzcI0Fdg8AyFAExaEK6Vy\n" + + "jP7SXGLwvfisw34OxuZr3qmx1Sufu4toH3XrB7QJN8XyqqbsGxUCBqWif9RSK4xj\n" + + "zRTe56iPeiSJJOIciMP9i2ldI+KgLycyeDvGoBj0HCLO3gVaBe4ubVrj5KjhX2PV\n" + + "NEJd3XZRzaXZE2aAMQ==\n" + + "=F9yX\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + public static final String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: D1A6 6E1A 23B1 82C9 980F 788C FBFC C82A 015E 7330\n" + + "Comment: Bob Babbage \n" + + "\n" + + "xcSYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + + "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + + "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + + "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + + "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + + "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + + "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + + "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + + "vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM\n" + + "cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK\n" + + "3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z\n" + + "Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs\n" + + "hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ\n" + + "bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4\n" + + "i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI\n" + + "1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP\n" + + "fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6\n" + + "fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E\n" + + "LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx\n" + + "+akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL\n" + + "hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN\n" + + "WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/\n" + + "MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC\n" + + "mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC\n" + + "YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E\n" + + "he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8\n" + + "zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P\n" + + "NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT\n" + + "t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qizSFCb2IgQmFiYmFnZSA8Ym9iQG9w\n" + + "ZW5wZ3AuZXhhbXBsZT7CwQ4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\n" + + "F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U\n" + + "2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX\n" + + "yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe\n" + + "doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3\n" + + "BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl\n" + + "sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN\n" + + "4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+\n" + + "L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG\n" + + "ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikbH\n" + + "xJgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD\n" + + "bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar\n" + + "29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2\n" + + "WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB\n" + + "leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te\n" + + "g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj\n" + + "Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn\n" + + "JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx\n" + + "IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp\n" + + "SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h\n" + + "OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np\n" + + "Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c\n" + + "+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0\n" + + "tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o\n" + + "BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny\n" + + "zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK\n" + + "clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl\n" + + "zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr\n" + + "gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ\n" + + "aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5\n" + + "fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/\n" + + "ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5\n" + + "HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf\n" + + "SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd\n" + + "5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ\n" + + "E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM\n" + + "GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY\n" + + "vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ\n" + + "26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hrCwPYEGAEKACAWIQTRpm4aI7GCyZgP\n" + + "eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX\n" + + "c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief\n" + + "rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0\n" + + "JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg\n" + + "71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH\n" + + "s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd\n" + + "NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91\n" + + "6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7\n" + + "xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=\n" + + "=FAzO\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + public static final String BOB_PRIMARY_FINGERPRINT = "D1A66E1A23B182C9980F788CFBFCC82A015E7330"; + public static final String BOB_SIGNING_FINGERPRINT = "D1A66E1A23B182C9980F788CFBFCC82A015E7330"; + + // 'Carol' key from draft-bre-openpgp-samples-00 + public static final String CAROL_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Comment: 71FF DA00 4409 E5DD B0C3 E8F1 9BA7 89DC 76D6 849A\n" + + "Comment: Carol Oldstyle \n" + + "\n" + + "xsPuBF3+CmgRDADZhdKTM3ms3XpXnQke83FgaIBtP1g1qhqpCfg50WiPS0kjiMC0\n" + + "OJz2vh59nusbBLzgI//Y1VMhKfIWYbqMcIY+lWbseHjl52rqW6AaJ0TH4NgVt7vh\n" + + "yVeJt0k/NnxvNhMd0587KXmfpDxrwBqc/l5cVB+p0rL8vs8kxojHXAi5V3koM0Uj\n" + + "REWs5Jpj/XU9LhEoyXZkeJC/pes1u6UKoFYn7dFIP49Kkd1kb+1bNfdPYtA0JpcG\n" + + "zYgeMNOvdWJwn43dNhxoeuXfmAEhA8LdzT0C0O+7akXOKWrfhXJ8MTBqvPgWZYx7\n" + + "MNuQx/ejIMZHl+Iaf7hG976ILH+NCGiKkhidd9GIuA/WteHiQbXLyfiQ4n8P12q9\n" + + "+4dq6ybUM65tnozRyyN+1m3rU2a/+Ly3JCh4TeO27w+cxMWkaeHyTQaJVMbMbDpX\n" + + "duVd32MA33UVNH5/KXMVczVi5asVjuKDSojJDV1QwX8izZNl1t+AI0L3balCabV0\n" + + "SFhlfnBEUj1my1sBAMOSO/I67BvBS3IPHZWXHjgclhs26mPzRlZLryAUWR2DDACH\n" + + "5fx+yUAdZ8Vu/2zWTHxwWJ/X6gGTLqa9CmfDq5UDqYFFzuWwN4HJ+ryOuak1CGwS\n" + + "KJUBSA75HExbv0naWg+suy+pEDvF0VALPU9VUkSQtHyR10YO2FWOe3AEtpbYDRwp\n" + + "dr1ZwEbb3L6IGQ5i/4CNHbJ2u3yUeXsDNAvrpVSEcIjA01RPCOKmf58SDZp4yDdP\n" + + "xGhM8w6a18+fdQr22f2cJ0xgfPlbzFbO+FUsEgKvn6QTLhbaYw4zs7rdQDejWHV8\n" + + "2hP4K+rb9FwknYdV9uo4m77MgGlU+4yvJnGEYaL3jwjI3bH9aooNOl6XbvVAzNzo\n" + + "mYmaTO7mp6xFAu43yuGyd9K+1E4k7CQTROxTZ+RdtQjV95hSsEmMg792nQvDSBW4\n" + + "xwfOQ7pf3kC7r9fm8u9nBlEN12HsbQ8Yvux/ld5q5RaIlD19jzfVR6+hJzbj2ZnU\n" + + "yQs4ksAfIHTzTdLttRxS9lTRTkVx2vbUnoSBy6TYF1mf6nRPpSm1riZxnkR4+BQL\n" + + "/0rUAxwegTNIG/5M612s2a45QvYK1turZ7spI1RGitJUIjBXUuR76jIsyqagIhBl\n" + + "5nEsQ4HLv8OQ3EgJ5T9gldLFpHNczLxBQnnNwfPoD2e0kC/iy0rfiNX8HWpTgQpb\n" + + "zAosLj5/E0iNlildynIhuqBosyRWFqGva0O6qioL90srlzlfKCloe9R9w3HizjCb\n" + + "f59yEspuJt9iHVNOPOW2Wj5ub0KTiJPp9vBmrFaB79/IlgojpQoYvQ77Hx5A9CJq\n" + + "paMCHGOW6Uz9euN1ozzETEkIPtL8XAxcogfpe2JKE1uS7ugxsKEGEDfxOQFKAGV0\n" + + "XFtIx50vFCr2vQro0WB858CGN47dCxChhNUxNtGc11JNEkNv/X7hKtRf/5VCmnaz\n" + + "GWwNK47cqZ7GJfEBnElD7s/tQvTC5Qp7lg9gEt47TUX0bjzUTCxNvLosuKL9+J1W\n" + + "ln1myRpff/5ZOAnZTPHR+AbX4bRB4sK5zijQe4139Dn2oRYK+EIYoBAxFxSOzehP\n" + + "IcKKBB8RCAA8BQJd/gppAwsJCgkQm6eJ3HbWhJoEFQoJCAIWAQIXgAIbAwIeARYh\n" + + "BHH/2gBECeXdsMPo8Zunidx21oSaAABihQD/VWnF1HbBhP+kLwWsqxuYjEslEsM2\n" + + "UQPeKGK9an8HZ78BAJPaiL3OpuOmsIoCfOghhMZOKXjIV+Z57LwaMw7FQfPgzSZD\n" + + "YXJvbCBPbGRzdHlsZSA8Y2Fyb2xAb3BlbnBncC5leGFtcGxlPsKKBBMRCAA8BQJd\n" + + "/gppAwsJCgkQm6eJ3HbWhJoEFQoJCAIWAQIXgAIbAwIeARYhBHH/2gBECeXdsMPo\n" + + "8Zunidx21oSaAABQTAD/ZMXAvSbKaMJJpAfwp1C7KAj6K2k2CAz5jwUXyGf1+jUA\n" + + "/2iAMiX1XcLy3n0L8ytzge8/UAFHafBl4rn4DmUugfhjzsPMBF3+CmgQDADZhdKT\n" + + "M3ms3XpXnQke83FgaIBtP1g1qhqpCfg50WiPS0kjiMC0OJz2vh59nusbBLzgI//Y\n" + + "1VMhKfIWYbqMcIY+lWbseHjl52rqW6AaJ0TH4NgVt7vhyVeJt0k/NnxvNhMd0587\n" + + "KXmfpDxrwBqc/l5cVB+p0rL8vs8kxojHXAi5V3koM0UjREWs5Jpj/XU9LhEoyXZk\n" + + "eJC/pes1u6UKoFYn7dFIP49Kkd1kb+1bNfdPYtA0JpcGzYgeMNOvdWJwn43dNhxo\n" + + "euXfmAEhA8LdzT0C0O+7akXOKWrfhXJ8MTBqvPgWZYx7MNuQx/ejIMZHl+Iaf7hG\n" + + "976ILH+NCGiKkhidd9GIuA/WteHiQbXLyfiQ4n8P12q9+4dq6ybUM65tnozRyyN+\n" + + "1m3rU2a/+Ly3JCh4TeO27w+cxMWkaeHyTQaJVMbMbDpXduVd32MA33UVNH5/KXMV\n" + + "czVi5asVjuKDSojJDV1QwX8izZNl1t+AI0L3balCabV0SFhlfnBEUj1my1sMAIfl\n" + + "/H7JQB1nxW7/bNZMfHBYn9fqAZMupr0KZ8OrlQOpgUXO5bA3gcn6vI65qTUIbBIo\n" + + "lQFIDvkcTFu/SdpaD6y7L6kQO8XRUAs9T1VSRJC0fJHXRg7YVY57cAS2ltgNHCl2\n" + + "vVnARtvcvogZDmL/gI0dsna7fJR5ewM0C+ulVIRwiMDTVE8I4qZ/nxINmnjIN0/E\n" + + "aEzzDprXz591CvbZ/ZwnTGB8+VvMVs74VSwSAq+fpBMuFtpjDjOzut1AN6NYdXza\n" + + "E/gr6tv0XCSdh1X26jibvsyAaVT7jK8mcYRhovePCMjdsf1qig06Xpdu9UDM3OiZ\n" + + "iZpM7uanrEUC7jfK4bJ30r7UTiTsJBNE7FNn5F21CNX3mFKwSYyDv3adC8NIFbjH\n" + + "B85Dul/eQLuv1+by72cGUQ3XYextDxi+7H+V3mrlFoiUPX2PN9VHr6EnNuPZmdTJ\n" + + "CziSwB8gdPNN0u21HFL2VNFORXHa9tSehIHLpNgXWZ/qdE+lKbWuJnGeRHj4FAv+\n" + + "MQaafW0uHF+N8MDm8UWPvf4Vd0UJ0UpIjRWl2hTV+BHkNfvZlBRhhQIphNiKRe/W\n" + + "ap0f/lW2Gm2uS0KgByjjNXEzTiwrte2GX65M6F6Lz8N31kt1Iig1xGOuv+6HmxTN\n" + + "R8gL2K5PdJeJn8PTJWrRS7+BY8Hdkgb+wVpzE5cCvpFiG/P0yqfBdLWxVPlPI7dc\n" + + "hDkmx4iAhHJX9J/gX/hC6L3AzPNJqNPAKy20wYp/ruTbbwBolW/4ikWij460JrvB\n" + + "sm6Sp81A3ebaiN9XkJygLOyhGyhMieGulCYz6AahAFcECtPXGTcordV1mJth8yjF\n" + + "4gZfDQyg0nMW4Yr49yeFXcRMUw1yzN3Q9v2zzqDuFi2lGYTXYmVqLYzM9KbLO2Wx\n" + + "E/21xnBjLsl09l/FdA/bhdZq3t4/apbFOeQQ/j/AphvzWbsJnhG9Q7+d3VoDlz0g\n" + + "FiSduCYIAAq8dUOJNjrUTkZsL1pOIjhYjCMi2uiKS6RQkT6nvuumPF/D/VTnUGeZ\n" + + "wooEGBEIADwFAl3+CmkDCwkKCRCbp4ncdtaEmgQVCgkIAhYBAheAAhsMAh4BFiEE\n" + + "cf/aAEQJ5d2ww+jxm6eJ3HbWhJoAAEEpAP91hFqmcb2ZqVcaRDMSVmhkEcFIRmpH\n" + + "vDoQtVn8sArWqwEAi8HwbMhL+YwRItRZDknpC4vFjTHVMd1zMrz/JyeuT9k=\n" + + "=pa/S\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + public static final String CAROL_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: 71FF DA00 4409 E5DD B0C3 E8F1 9BA7 89DC 76D6 849A\n" + + "Comment: Carol Oldstyle \n" + + "\n" + + "xcQTBF3+CmgRDADZhdKTM3ms3XpXnQke83FgaIBtP1g1qhqpCfg50WiPS0kjiMC0\n" + + "OJz2vh59nusbBLzgI//Y1VMhKfIWYbqMcIY+lWbseHjl52rqW6AaJ0TH4NgVt7vh\n" + + "yVeJt0k/NnxvNhMd0587KXmfpDxrwBqc/l5cVB+p0rL8vs8kxojHXAi5V3koM0Uj\n" + + "REWs5Jpj/XU9LhEoyXZkeJC/pes1u6UKoFYn7dFIP49Kkd1kb+1bNfdPYtA0JpcG\n" + + "zYgeMNOvdWJwn43dNhxoeuXfmAEhA8LdzT0C0O+7akXOKWrfhXJ8MTBqvPgWZYx7\n" + + "MNuQx/ejIMZHl+Iaf7hG976ILH+NCGiKkhidd9GIuA/WteHiQbXLyfiQ4n8P12q9\n" + + "+4dq6ybUM65tnozRyyN+1m3rU2a/+Ly3JCh4TeO27w+cxMWkaeHyTQaJVMbMbDpX\n" + + "duVd32MA33UVNH5/KXMVczVi5asVjuKDSojJDV1QwX8izZNl1t+AI0L3balCabV0\n" + + "SFhlfnBEUj1my1sBAMOSO/I67BvBS3IPHZWXHjgclhs26mPzRlZLryAUWR2DDACH\n" + + "5fx+yUAdZ8Vu/2zWTHxwWJ/X6gGTLqa9CmfDq5UDqYFFzuWwN4HJ+ryOuak1CGwS\n" + + "KJUBSA75HExbv0naWg+suy+pEDvF0VALPU9VUkSQtHyR10YO2FWOe3AEtpbYDRwp\n" + + "dr1ZwEbb3L6IGQ5i/4CNHbJ2u3yUeXsDNAvrpVSEcIjA01RPCOKmf58SDZp4yDdP\n" + + "xGhM8w6a18+fdQr22f2cJ0xgfPlbzFbO+FUsEgKvn6QTLhbaYw4zs7rdQDejWHV8\n" + + "2hP4K+rb9FwknYdV9uo4m77MgGlU+4yvJnGEYaL3jwjI3bH9aooNOl6XbvVAzNzo\n" + + "mYmaTO7mp6xFAu43yuGyd9K+1E4k7CQTROxTZ+RdtQjV95hSsEmMg792nQvDSBW4\n" + + "xwfOQ7pf3kC7r9fm8u9nBlEN12HsbQ8Yvux/ld5q5RaIlD19jzfVR6+hJzbj2ZnU\n" + + "yQs4ksAfIHTzTdLttRxS9lTRTkVx2vbUnoSBy6TYF1mf6nRPpSm1riZxnkR4+BQL\n" + + "/0rUAxwegTNIG/5M612s2a45QvYK1turZ7spI1RGitJUIjBXUuR76jIsyqagIhBl\n" + + "5nEsQ4HLv8OQ3EgJ5T9gldLFpHNczLxBQnnNwfPoD2e0kC/iy0rfiNX8HWpTgQpb\n" + + "zAosLj5/E0iNlildynIhuqBosyRWFqGva0O6qioL90srlzlfKCloe9R9w3HizjCb\n" + + "f59yEspuJt9iHVNOPOW2Wj5ub0KTiJPp9vBmrFaB79/IlgojpQoYvQ77Hx5A9CJq\n" + + "paMCHGOW6Uz9euN1ozzETEkIPtL8XAxcogfpe2JKE1uS7ugxsKEGEDfxOQFKAGV0\n" + + "XFtIx50vFCr2vQro0WB858CGN47dCxChhNUxNtGc11JNEkNv/X7hKtRf/5VCmnaz\n" + + "GWwNK47cqZ7GJfEBnElD7s/tQvTC5Qp7lg9gEt47TUX0bjzUTCxNvLosuKL9+J1W\n" + + "ln1myRpff/5ZOAnZTPHR+AbX4bRB4sK5zijQe4139Dn2oRYK+EIYoBAxFxSOzehP\n" + + "IQAA/2BCN5HryGjVff2t7Q6fVrQQS9hsMisszZl5rWwUOO6zETHCigQfEQgAPAUC\n" + + "Xf4KaQMLCQoJEJunidx21oSaBBUKCQgCFgECF4ACGwMCHgEWIQRx/9oARAnl3bDD\n" + + "6PGbp4ncdtaEmgAAYoUA/1VpxdR2wYT/pC8FrKsbmIxLJRLDNlED3ihivWp/B2e/\n" + + "AQCT2oi9zqbjprCKAnzoIYTGTil4yFfmeey8GjMOxUHz4M0mQ2Fyb2wgT2xkc3R5\n" + + "bGUgPGNhcm9sQG9wZW5wZ3AuZXhhbXBsZT7CigQTEQgAPAUCXf4KaQMLCQoJEJun\n" + + "idx21oSaBBUKCQgCFgECF4ACGwMCHgEWIQRx/9oARAnl3bDD6PGbp4ncdtaEmgAA\n" + + "UEwA/2TFwL0mymjCSaQH8KdQuygI+itpNggM+Y8FF8hn9fo1AP9ogDIl9V3C8t59\n" + + "C/Mrc4HvP1ABR2nwZeK5+A5lLoH4Y8fD8QRd/gpoEAwA2YXSkzN5rN16V50JHvNx\n" + + "YGiAbT9YNaoaqQn4OdFoj0tJI4jAtDic9r4efZ7rGwS84CP/2NVTISnyFmG6jHCG\n" + + "PpVm7Hh45edq6lugGidEx+DYFbe74clXibdJPzZ8bzYTHdOfOyl5n6Q8a8AanP5e\n" + + "XFQfqdKy/L7PJMaIx1wIuVd5KDNFI0RFrOSaY/11PS4RKMl2ZHiQv6XrNbulCqBW\n" + + "J+3RSD+PSpHdZG/tWzX3T2LQNCaXBs2IHjDTr3VicJ+N3TYcaHrl35gBIQPC3c09\n" + + "AtDvu2pFzilq34VyfDEwarz4FmWMezDbkMf3oyDGR5fiGn+4Rve+iCx/jQhoipIY\n" + + "nXfRiLgP1rXh4kG1y8n4kOJ/D9dqvfuHausm1DOubZ6M0csjftZt61Nmv/i8tyQo\n" + + "eE3jtu8PnMTFpGnh8k0GiVTGzGw6V3blXd9jAN91FTR+fylzFXM1YuWrFY7ig0qI\n" + + "yQ1dUMF/Is2TZdbfgCNC922pQmm1dEhYZX5wRFI9ZstbDACH5fx+yUAdZ8Vu/2zW\n" + + "THxwWJ/X6gGTLqa9CmfDq5UDqYFFzuWwN4HJ+ryOuak1CGwSKJUBSA75HExbv0na\n" + + "Wg+suy+pEDvF0VALPU9VUkSQtHyR10YO2FWOe3AEtpbYDRwpdr1ZwEbb3L6IGQ5i\n" + + "/4CNHbJ2u3yUeXsDNAvrpVSEcIjA01RPCOKmf58SDZp4yDdPxGhM8w6a18+fdQr2\n" + + "2f2cJ0xgfPlbzFbO+FUsEgKvn6QTLhbaYw4zs7rdQDejWHV82hP4K+rb9FwknYdV\n" + + "9uo4m77MgGlU+4yvJnGEYaL3jwjI3bH9aooNOl6XbvVAzNzomYmaTO7mp6xFAu43\n" + + "yuGyd9K+1E4k7CQTROxTZ+RdtQjV95hSsEmMg792nQvDSBW4xwfOQ7pf3kC7r9fm\n" + + "8u9nBlEN12HsbQ8Yvux/ld5q5RaIlD19jzfVR6+hJzbj2ZnUyQs4ksAfIHTzTdLt\n" + + "tRxS9lTRTkVx2vbUnoSBy6TYF1mf6nRPpSm1riZxnkR4+BQL/jEGmn1tLhxfjfDA\n" + + "5vFFj73+FXdFCdFKSI0VpdoU1fgR5DX72ZQUYYUCKYTYikXv1mqdH/5VthptrktC\n" + + "oAco4zVxM04sK7Xthl+uTOhei8/Dd9ZLdSIoNcRjrr/uh5sUzUfIC9iuT3SXiZ/D\n" + + "0yVq0Uu/gWPB3ZIG/sFacxOXAr6RYhvz9MqnwXS1sVT5TyO3XIQ5JseIgIRyV/Sf\n" + + "4F/4Qui9wMzzSajTwCsttMGKf67k228AaJVv+IpFoo+OtCa7wbJukqfNQN3m2ojf\n" + + "V5CcoCzsoRsoTInhrpQmM+gGoQBXBArT1xk3KK3VdZibYfMoxeIGXw0MoNJzFuGK\n" + + "+PcnhV3ETFMNcszd0Pb9s86g7hYtpRmE12Jlai2MzPSmyztlsRP9tcZwYy7JdPZf\n" + + "xXQP24XWat7eP2qWxTnkEP4/wKYb81m7CZ4RvUO/nd1aA5c9IBYknbgmCAAKvHVD\n" + + "iTY61E5GbC9aTiI4WIwjItroikukUJE+p77rpjxfw/1U51BnmQAA/ih5jIthn2ZE\n" + + "r1YoOsUs8CBhylTsRZK6VS4ZCErcyl2tD2LCigQYEQgAPAUCXf4KaQMLCQoJEJun\n" + + "idx21oSaBBUKCQgCFgECF4ACGwwCHgEWIQRx/9oARAnl3bDD6PGbp4ncdtaEmgAA\n" + + "QSkA/3WEWqZxvZmpVxpEMxJWaGQRwUhGake8OhC1WfywCtarAQCLwfBsyEv5jBEi\n" + + "1FkOSekLi8WNMdUx3XMyvP8nJ65P2Q==\n" + + "=Xj8h\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + public static final String CAROL_PRIMARY_FINGERPRINT = "71FFDA004409E5DDB0C3E8F19BA789DC76D6849A"; + public static final String CAROL_SIGNING_FINGERPRINT = "71FFDA004409E5DDB0C3E8F19BA789DC76D6849A"; + + public static final String PASSWORD_PROTECTED_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: FC63 688A 5E69 8C29 40AF 7029 7C62 2B00 D459 2657\n" + + "Comment: Password Protected \n" + + "\n" + + "xYYEY8qfmxYJKwYBBAHaRw8BAQdAv5atAPgP3WOvjoeEGAXIpX+k9LbX1+roEQQE\n" + + "WaQfbMv+CQMI7d4yuArkBqz/J/UllaSoHN2kYdJE4Biiqgto2d39B8JRCrb0LSeX\n" + + "25TolXynV3bdiTsVKtnNOOcCzP09kDMu8uCMpregFrMdI511iR+dysLAEQQfFgoA\n" + + "gwWCY8qfmwWJBZ+mAAMLCQcJEHxiKwDUWSZXRxQAAAAAAB4AIHNhbHRAbm90YXRp\n" + + "b25zLnNlcXVvaWEtcGdwLm9yZ5Rt+kxLFFiFbTaZO2Rbf52K6FEcetqiht8jk9Vt\n" + + "DObSAxUKCAKbAQIeARYhBPxjaIpeaYwpQK9wKXxiKwDUWSZXAABTzQEA9Vy2e5eU\n" + + "dFj+gfwPULtwEJqMpj29eN37J0VfwF1RdW0BAMeXutE1dzL5PdIIX8VJAIv9RXVR\n" + + "lw5TujtjLhr8uzEKzSpQYXNzd29yZCBQcm90ZWN0ZWQgPHByb3RlY3RlZEBvcGVu\n" + + "cGdwLm9yZz7CwBQEExYKAIYFgmPKn5sFiQWfpgADCwkHCRB8YisA1FkmV0cUAAAA\n" + + "AAAeACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdTOuFjGL7cyOIVEfem\n" + + "5b/gCJLP6LFKTy3P/gFGRB7VEAMVCggCmQECmwECHgEWIQT8Y2iKXmmMKUCvcCl8\n" + + "YisA1FkmVwAARXsBAP4jwRWnAqEe59BV+0WviYzC8NhKpIjXwRQIM5yD6E90AQCQ\n" + + "wfhqsexB2rVQGw0siW2c/3DUhmnK8osNK5f8iLv5BceGBGPKn5sWCSsGAQQB2kcP\n" + + "AQEHQM/fv1zxwMjruKiq9W7PcMUbcMKQ3lbFdqPtwEJ16LxY/gkDCA3yM1VPvA6b\n" + + "/1vqf8sxU96j7CAMZaQRutdRd1xwFxx9ZIvhrPjm23nCcURzmnPflnKdx/p8/QVj\n" + + "jTQufQbnZkrmo/fg+eZURLX6O3Op2svCwMUEGBYKATcFgmPKn5sFiQWfpgAJEHxi\n" + + "KwDUWSZXRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZ0X3\n" + + "iVnya1OCsmkt7OijGLXSTv9FRbFVf+fcQGSMzViBApsCvqAEGRYKAG8FgmPKn5sJ\n" + + "EGiGL7kPBxZbRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9y\n" + + "Z62UKDMX/Uh2ywrGJjKi3Ex6jpghyED+kPlNuxS8uMs6FiEE2PHLwmEzUNGnZtNf\n" + + "aIYvuQ8HFlsAAPgiAPwIlVOxTF7J80KAiHrApEgfLHsEeGivjEtnkKO6eUa2awEA\n" + + "5qlATwB3bQVkMFYa893MxrjVmmasil81uwMiU8gtRQoWIQT8Y2iKXmmMKUCvcCl8\n" + + "YisA1FkmVwAAktwBAOEXjAXOZaFM8PoSNtrKVLakPXCadY8zduAqqgmp5PBwAP0R\n" + + "EpO9g0mQuCCmg6eeXm2GxChWORWArh9of7l/epycAceLBGPKn5sSCisGAQQBl1UB\n" + + "BQEBB0DDEzY37G8GNXIJqbVsawutIqNTZcizObXrau9F0H5wHQMBCAf+CQMI9ppA\n" + + "+RYt5Sv/gIPNmVm7UraBpK75qOC/tN9h/uNaaadcgrWEXMr6+YWjvBmH+iCV61/y\n" + + "b9Gkfxn2V/lw8asgch86Y6tN0Rhy+uXTFKMHecLABgQYFgoAeAWCY8qfmwWJBZ+m\n" + + "AAkQfGIrANRZJldHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Au\n" + + "b3JnYeL3YzJjnG3vwSjzVnzgbFCe5QyC0/mFnqML7+hQi0kCmwwWIQT8Y2iKXmmM\n" + + "KUCvcCl8YisA1FkmVwAAbRcA/3haEwnnHhitQNbvDs2DqzVvz0QtjEW59ZKFgzX2\n" + + "PUMXAQDJzcz9GoPTqU8hioiSBoQUjN883qv6sJHiEveRyDbMDQ==\n" + + "=xHUd\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + public static final String PASSWORD_PROTECTED_CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Comment: FC63 688A 5E69 8C29 40AF 7029 7C62 2B00 D459 2657\n" + + "Comment: Password Protected \n" + + "\n" + + "xjMEY8qfmxYJKwYBBAHaRw8BAQdAv5atAPgP3WOvjoeEGAXIpX+k9LbX1+roEQQE\n" + + "WaQfbMvCwBEEHxYKAIMFgmPKn5sFiQWfpgADCwkHCRB8YisA1FkmV0cUAAAAAAAe\n" + + "ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmeUbfpMSxRYhW02mTtkW3+d\n" + + "iuhRHHraoobfI5PVbQzm0gMVCggCmwECHgEWIQT8Y2iKXmmMKUCvcCl8YisA1Fkm\n" + + "VwAAU80BAPVctnuXlHRY/oH8D1C7cBCajKY9vXjd+ydFX8BdUXVtAQDHl7rRNXcy\n" + + "+T3SCF/FSQCL/UV1UZcOU7o7Yy4a/LsxCs0qUGFzc3dvcmQgUHJvdGVjdGVkIDxw\n" + + "cm90ZWN0ZWRAb3BlbnBncC5vcmc+wsAUBBMWCgCGBYJjyp+bBYkFn6YAAwsJBwkQ\n" + + "fGIrANRZJldHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3Jn\n" + + "UzrhYxi+3MjiFRH3puW/4AiSz+ixSk8tz/4BRkQe1RADFQoIApkBApsBAh4BFiEE\n" + + "/GNoil5pjClAr3ApfGIrANRZJlcAAEV7AQD+I8EVpwKhHufQVftFr4mMwvDYSqSI\n" + + "18EUCDOcg+hPdAEAkMH4arHsQdq1UBsNLIltnP9w1IZpyvKLDSuX/Ii7+QXOMwRj\n" + + "yp+bFgkrBgEEAdpHDwEBB0DP379c8cDI67ioqvVuz3DFG3DCkN5WxXaj7cBCdei8\n" + + "WMLAxQQYFgoBNwWCY8qfmwWJBZ+mAAkQfGIrANRZJldHFAAAAAAAHgAgc2FsdEBu\n" + + "b3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnRfeJWfJrU4KyaS3s6KMYtdJO/0VFsVV/\n" + + "59xAZIzNWIECmwK+oAQZFgoAbwWCY8qfmwkQaIYvuQ8HFltHFAAAAAAAHgAgc2Fs\n" + + "dEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnrZQoMxf9SHbLCsYmMqLcTHqOmCHI\n" + + "QP6Q+U27FLy4yzoWIQTY8cvCYTNQ0adm019ohi+5DwcWWwAA+CIA/AiVU7FMXsnz\n" + + "QoCIesCkSB8sewR4aK+MS2eQo7p5RrZrAQDmqUBPAHdtBWQwVhrz3czGuNWaZqyK\n" + + "XzW7AyJTyC1FChYhBPxjaIpeaYwpQK9wKXxiKwDUWSZXAACS3AEA4ReMBc5loUzw\n" + + "+hI22spUtqQ9cJp1jzN24CqqCank8HAA/RESk72DSZC4IKaDp55ebYbEKFY5FYCu\n" + + "H2h/uX96nJwBzjgEY8qfmxIKKwYBBAGXVQEFAQEHQMMTNjfsbwY1cgmptWxrC60i\n" + + "o1NlyLM5tetq70XQfnAdAwEIB8LABgQYFgoAeAWCY8qfmwWJBZ+mAAkQfGIrANRZ\n" + + "JldHFAAAAAAAHgAgc2FsdEBub3RhdGlvbnMuc2VxdW9pYS1wZ3Aub3JnYeL3YzJj\n" + + "nG3vwSjzVnzgbFCe5QyC0/mFnqML7+hQi0kCmwwWIQT8Y2iKXmmMKUCvcCl8YisA\n" + + "1FkmVwAAbRcA/3haEwnnHhitQNbvDs2DqzVvz0QtjEW59ZKFgzX2PUMXAQDJzcz9\n" + + "GoPTqU8hioiSBoQUjN883qv6sJHiEveRyDbMDQ==\n" + + "=xlgc\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + public static final String PASSWORD = "sw0rdf1sh"; + public static final String PASSWORD_PROTECTED_PRIMARY_FINGERPRINT = "FC63688A5E698C2940AF70297C622B00D4592657"; + public static final String PASSWORD_PROTECTED_SIGNING_FINGERPRINT = "D8F1CBC2613350D1A766D35F68862FB90F07165B"; + + + public static final byte[] BEGIN_PGP_PRIVATE_KEY_BLOCK = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n".getBytes(StandardCharsets.UTF_8); + public static final byte[] END_PGP_PRIVATE_KEY_BLOCK = "-----END PGP PRIVATE KEY BLOCK-----".getBytes(StandardCharsets.UTF_8); + public static final byte[] BEGIN_PGP_PUBLIC_KEY_BLOCK = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n".getBytes(StandardCharsets.UTF_8); + public static final byte[] END_PGP_PUBLIC_KEY_BLOCK = "-----END PGP PUBLIC KEY BLOCK-----".getBytes(StandardCharsets.UTF_8); + public static final byte[] BEGIN_PGP_MESSAGE = "-----BEGIN PGP MESSAGE-----\n".getBytes(StandardCharsets.UTF_8); + public static final byte[] END_PGP_MESSAGE = "-----END PGP MESSAGE-----".getBytes(StandardCharsets.UTF_8); + public static final byte[] BEGIN_PGP_SIGNATURE = "-----BEGIN PGP SIGNATURE-----\n".getBytes(StandardCharsets.UTF_8); + public static final byte[] END_PGP_SIGNATURE = "-----END PGP SIGNATURE-----".getBytes(StandardCharsets.UTF_8); + public static final byte[] BEGIN_PGP_SIGNED_MESSAGE = "-----BEGIN PGP SIGNED MESSAGE-----\n".getBytes(StandardCharsets.UTF_8); + + private static Date parseUTCDate(String utcFormatted) { + try { + return UTCUtil.parseUTCDate(utcFormatted); + } catch (ParseException e) { + throw new IllegalArgumentException("Malformed UTC timestamp.", e); + } + } +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/SopExecutionAssertions.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/SopExecutionAssertions.java new file mode 100644 index 0000000..bd07f0b --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/SopExecutionAssertions.java @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.assertions; + +import sop.exception.SOPGPException; + +import java.util.function.IntSupplier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +/** + * DSL for testing the return values of SOP method calls. + */ +public class SopExecutionAssertions { + + /** + * Assert that the execution of the given function returns 0. + * + * @param function function to execute + */ + public static void assertSuccess(IntSupplier function) { + assertEquals(0, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns a generic error with error code 1. + * + * @param function function to execute. + */ + public static void assertGenericError(IntSupplier function) { + assertEquals(1, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns a non-zero error code. + * + * @param function function to execute + */ + public static void assertAnyError(IntSupplier function) { + assertNotEquals(0, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 3 + * (which corresponds to {@link sop.exception.SOPGPException.NoSignature}). + * + * @param function function to execute. + */ + public static void assertNoSignature(IntSupplier function) { + assertEquals(SOPGPException.NoSignature.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 13 + * (which corresponds to {@link sop.exception.SOPGPException.UnsupportedAsymmetricAlgo}). + * + * @param function function to execute. + */ + public static void assertUnsupportedAsymmetricAlgo(IntSupplier function) { + assertEquals(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 17 + * (which corresponds to {@link sop.exception.SOPGPException.CertCannotEncrypt}). + * + * @param function function to execute. + */ + public static void assertCertCannotEncrypt(IntSupplier function) { + assertEquals(SOPGPException.CertCannotEncrypt.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 19 + * (which corresponds to {@link sop.exception.SOPGPException.MissingArg}). + * + * @param function function to execute. + */ + public static void assertMissingArg(IntSupplier function) { + assertEquals(SOPGPException.MissingArg.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 23 + * (which corresponds to {@link sop.exception.SOPGPException.IncompleteVerification}). + * + * @param function function to execute. + */ + public static void assertIncompleteVerification(IntSupplier function) { + assertEquals(SOPGPException.IncompleteVerification.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 29 + * (which corresponds to {@link sop.exception.SOPGPException.CannotDecrypt}). + * + * @param function function to execute. + */ + public static void assertCannotDecrypt(IntSupplier function) { + assertEquals(SOPGPException.CannotDecrypt.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 31 + * (which corresponds to {@link sop.exception.SOPGPException.PasswordNotHumanReadable}). + * + * @param function function to execute. + */ + public static void assertPasswordNotHumanReadable(IntSupplier function) { + assertEquals(SOPGPException.PasswordNotHumanReadable.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 37 + * (which corresponds to {@link sop.exception.SOPGPException.UnsupportedOption}). + * + * @param function function to execute. + */ + public static void assertUnsupportedOption(IntSupplier function) { + assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 41 + * (which corresponds to {@link sop.exception.SOPGPException.BadData}). + * + * @param function function to execute. + */ + public static void assertBadData(IntSupplier function) { + assertEquals(SOPGPException.BadData.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 53 + * (which corresponds to {@link sop.exception.SOPGPException.ExpectedText}). + * + * @param function function to execute. + */ + public static void assertExpectedText(IntSupplier function) { + assertEquals(SOPGPException.ExpectedText.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 59 + * (which corresponds to {@link sop.exception.SOPGPException.OutputExists}). + * + * @param function function to execute. + */ + public static void assertOutputExists(IntSupplier function) { + assertEquals(SOPGPException.OutputExists.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 61 + * (which corresponds to {@link sop.exception.SOPGPException.MissingInput}). + * + * @param function function to execute. + */ + public static void assertMissingInput(IntSupplier function) { + assertEquals(SOPGPException.MissingInput.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 67 + * (which corresponds to {@link sop.exception.SOPGPException.KeyIsProtected}). + * + * @param function function to execute. + */ + public static void assertKeyIsProtected(IntSupplier function) { + assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 69 + * (which corresponds to {@link sop.exception.SOPGPException.UnsupportedSubcommand}). + * + * @param function function to execute. + */ + public static void assertUnsupportedSubcommand(IntSupplier function) { + assertEquals(SOPGPException.UnsupportedSubcommand.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 71 + * (which corresponds to {@link sop.exception.SOPGPException.UnsupportedSpecialPrefix}). + * + * @param function function to execute. + */ + public static void assertUnsupportedSpecialPrefix(IntSupplier function) { + assertEquals(SOPGPException.UnsupportedSpecialPrefix.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 73 + * (which corresponds to {@link sop.exception.SOPGPException.AmbiguousInput}). + * + * @param function function to execute. + */ + public static void assertAmbiguousInput(IntSupplier function) { + assertEquals(SOPGPException.AmbiguousInput.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 79 + * (which corresponds to {@link sop.exception.SOPGPException.KeyCannotSign}). + * + * @param function function to execute. + */ + public static void assertKeyCannotSign(IntSupplier function) { + assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 83 + * (which corresponds to {@link sop.exception.SOPGPException.IncompatibleOptions}). + * + * @param function function to execute. + */ + public static void assertIncompatibleOptions(IntSupplier function) { + assertEquals(SOPGPException.IncompatibleOptions.EXIT_CODE, function.getAsInt()); + } + + /** + * Assert that the execution of the given function returns error code 89 + * (which corresponds to {@link sop.exception.SOPGPException.UnsupportedProfile}). + * + * @param function function to execute. + */ + public static void assertUnsupportedProfile(IntSupplier function) { + assertEquals(SOPGPException.UnsupportedProfile.EXIT_CODE, function.getAsInt()); + } +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java new file mode 100644 index 0000000..dea8717 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.assertions; + +import sop.Verification; +import sop.enums.SignatureMode; +import sop.testsuite.JUtils; + +import java.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 { + + private final Verification verification; + + public static VerificationAssert assertThatVerification(Verification verification) { + return new VerificationAssert(verification); + } + + private VerificationAssert(Verification verification) { + this.verification = verification; + } + + public VerificationAssert issuedBy(String signingKeyFingerprint, String primaryFingerprint) { + return isBySigningKey(signingKeyFingerprint) + .issuedBy(primaryFingerprint); + } + + public VerificationAssert issuedBy(String primaryFingerprint) { + assertEquals(primaryFingerprint, verification.getSigningCertFingerprint()); + return this; + } + + public VerificationAssert isBySigningKey(String signingKeyFingerprint) { + assertEquals(signingKeyFingerprint, verification.getSigningKeyFingerprint()); + return this; + } + + public VerificationAssert isCreatedAt(Date creationDate) { + JUtils.assertDateEquals(creationDate, verification.getCreationTime()); + return this; + } + + public VerificationAssert hasDescription(String description) { + assertEquals(description, verification.getJsonOrDescription().get()); + return this; + } + + public VerificationAssert hasDescriptionOrNull(String description) { + if (verification.getJsonOrDescription().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; + } + + public VerificationAssert hasModeOrNull(SignatureMode mode) { + if (verification.getSignatureMode().isEmpty()) { + return this; + } + return hasMode(mode); + } +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationListAssert.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationListAssert.java new file mode 100644 index 0000000..6c90609 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationListAssert.java @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.assertions; + +import sop.Verification; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public final class VerificationListAssert { + + private final List verificationList = new ArrayList<>(); + + private VerificationListAssert(List verifications) { + this.verificationList.addAll(verifications); + } + + public static VerificationListAssert assertThatVerificationList(List verifications) { + return new VerificationListAssert(verifications); + } + + public VerificationListAssert isEmpty() { + assertTrue(verificationList.isEmpty()); + return this; + } + + public VerificationListAssert isNotEmpty() { + assertFalse(verificationList.isEmpty()); + return this; + } + + public VerificationListAssert sizeEquals(int size) { + assertEquals(size, verificationList.size()); + return this; + } + + public VerificationAssert hasSingleItem() { + sizeEquals(1); + return VerificationAssert.assertThatVerification(verificationList.get(0)); + } + + public VerificationListAssert containsVerificationByCert(String primaryFingerprint) { + for (Verification verification : verificationList) { + if (primaryFingerprint.equals(verification.getSigningCertFingerprint())) { + return this; + } + } + fail("No verification was issued by certificate " + primaryFingerprint); + return this; + } + + public VerificationListAssert containsVerificationBy(String signingKeyFingerprint, String primaryFingerprint) { + for (Verification verification : verificationList) { + if (primaryFingerprint.equals(verification.getSigningCertFingerprint()) && + signingKeyFingerprint.equals(verification.getSigningKeyFingerprint())) { + return this; + } + } + + fail("No verification was issued by key " + signingKeyFingerprint + " of cert " + primaryFingerprint); + return this; + } +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/package-info.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/package-info.java new file mode 100644 index 0000000..8e2ef7c --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * DSL for assertions on SOP objects. + */ +package sop.testsuite.assertions; diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java new file mode 100644 index 0000000..16ae256 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +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; +import java.util.ArrayList; +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<>(); + + static { + initBackends(); + } + + // populate instances list via configured test subject factory + private static void initBackends() { + String factoryName = System.getenv("test.implementation"); + if (factoryName == null) { + return; + } + + SOPInstanceFactory factory; + try { + Class testSubjectFactoryClass = Class.forName(factoryName); + factory = (SOPInstanceFactory) testSubjectFactoryClass + .getDeclaredConstructor().newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | + InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + + Map testSubjects = factory.provideSOPInstances(); + for (String key : testSubjects.keySet()) { + backends.add(Arguments.of(Named.of(key, testSubjects.get(key)))); + } + } + + 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(); + } + + public static boolean hasBackends() { + return !backends.isEmpty(); + } + +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java new file mode 100644 index 0000000..00488e1 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java @@ -0,0 +1,243 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +import org.junit.jupiter.api.Assertions; +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.testsuite.JUtils; +import sop.testsuite.TestData; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class ArmorDearmorTest extends AbstractSOPTest { + + static Stream provideInstances() { + return AbstractSOPTest.provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void dearmorArmorAliceKey(SOP sop) throws IOException { + byte[] aliceKey = TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8); + + byte[] dearmored = assumeSupported(sop::dearmor) + .data(aliceKey) + .getBytes(); + + Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK)); + + byte[] armored = assumeSupported(sop::armor) + .data(dearmored) + .getBytes(); + + JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); + JUtils.assertArrayEndsWithIgnoreNewlines(armored, TestData.END_PGP_PRIVATE_KEY_BLOCK); + + // assertAsciiArmorEquals(aliceKey, armored); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void dearmorArmorAliceCert(SOP sop) throws IOException { + byte[] aliceCert = TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8); + + byte[] dearmored = assumeSupported(sop::dearmor) + .data(aliceCert) + .getBytes(); + + Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); + + byte[] armored = assumeSupported(sop::armor) + .data(dearmored) + .getBytes(); + + JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK); + JUtils.assertArrayEndsWithIgnoreNewlines(armored, TestData.END_PGP_PUBLIC_KEY_BLOCK); + + // assertAsciiArmorEquals(aliceCert, armored); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void dearmorArmorBobKey(SOP sop) throws IOException { + byte[] bobKey = TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8); + + byte[] dearmored = assumeSupported(sop::dearmor) + .data(bobKey) + .getBytes(); + + Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK)); + + byte[] armored = assumeSupported(sop::armor) + .data(dearmored) + .getBytes(); + + JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); + JUtils.assertArrayEndsWithIgnoreNewlines(armored, TestData.END_PGP_PRIVATE_KEY_BLOCK); + + // assertAsciiArmorEquals(bobKey, armored); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void dearmorArmorBobCert(SOP sop) throws IOException { + byte[] bobCert = TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8); + + byte[] dearmored = assumeSupported(sop::dearmor) + .data(bobCert) + .getBytes(); + + Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); + + byte[] armored = assumeSupported(sop::armor) + .data(dearmored) + .getBytes(); + + JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK); + JUtils.assertArrayEndsWithIgnoreNewlines(armored, TestData.END_PGP_PUBLIC_KEY_BLOCK); + + // assertAsciiArmorEquals(bobCert, armored); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void dearmorArmorCarolKey(SOP sop) throws IOException { + byte[] carolKey = TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8); + + byte[] dearmored = assumeSupported(sop::dearmor) + .data(carolKey) + .getBytes(); + + Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK)); + + byte[] armored = assumeSupported(sop::armor) + .data(dearmored) + .getBytes(); + + JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); + JUtils.assertArrayEndsWithIgnoreNewlines(armored, TestData.END_PGP_PRIVATE_KEY_BLOCK); + + // assertAsciiArmorEquals(carolKey, armored); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void dearmorArmorCarolCert(SOP sop) throws IOException { + byte[] carolCert = TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8); + + byte[] dearmored = assumeSupported(sop::dearmor) + .data(carolCert) + .getBytes(); + + Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); + + byte[] armored = assumeSupported(sop::armor) + .data(dearmored) + .getBytes(); + + JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK); + JUtils.assertArrayEndsWithIgnoreNewlines(armored, TestData.END_PGP_PUBLIC_KEY_BLOCK); + + // assertAsciiArmorEquals(carolCert, armored); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void dearmorArmorMessage(SOP sop) throws IOException { + byte[] message = ("-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wV4DR2b2udXyHrYSAQdAMZy9Iqb1IxszjI3v+TsfK//0lnJ9PKHDqVAB5ohp+RMw\n" + + "8fmuL3phS9uISFT/DrizC8ALJhMqw5R+lLB/RvTTA/qS6tN5dRyL+YLFU3/N0CRF\n" + + "0j8BtQEsMmRo60LzUq/OBI0dFjwFq1efpfOGkpRYkuIzndCjBEgnLUkrHzUc1uD9\n" + + "CePQFpprprnGEzpE3flQLUc=\n" + + "=ZiFR\n" + + "-----END PGP MESSAGE-----\n").getBytes(StandardCharsets.UTF_8); + byte[] dearmored = assumeSupported(sop::dearmor) + .data(message) + .getBytes(); + + Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_MESSAGE)); + + byte[] armored = assumeSupported(sop::armor) + .data(dearmored) + .getBytes(); + + JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_MESSAGE); + JUtils.assertArrayEndsWithIgnoreNewlines(armored, TestData.END_PGP_MESSAGE); + + // assertAsciiArmorEquals(message, armored); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void dearmorArmorSignature(SOP sop) throws IOException { + byte[] signature = ("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wr0EABYKAG8FgmPBdRAJEPIxVQxPR+OORxQAAAAAAB4AIHNhbHRAbm90YXRpb25z\n" + + "LnNlcXVvaWEtcGdwLm9yZ2un17fF3C46Adgzp0mU4RG8Txy/T/zOBcBw/NYaLGrQ\n" + + "FiEE64W7X6M6deFelE5j8jFVDE9H444AAMiEAP9LBQWLo4oP5IrFZPuSUQSPsUxB\n" + + "c+Qu1raXDKzS/8Q9IAD+LnHIjRHcqNPobNHXF/saXIYXeZR+LJKszTJozzwqdQE=\n" + + "=GHvQ\n" + + "-----END PGP SIGNATURE-----\n").getBytes(StandardCharsets.UTF_8); + + byte[] dearmored = assumeSupported(sop::dearmor) + .data(signature) + .getBytes(); + + Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_SIGNATURE)); + + byte[] armored = assumeSupported(sop::armor) + .data(dearmored) + .getBytes(); + + JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_SIGNATURE); + JUtils.assertArrayEndsWithIgnoreNewlines(armored, TestData.END_PGP_SIGNATURE); + + JUtils.assertAsciiArmorEquals(signature, armored); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testDearmoringTwiceIsIdempotent(SOP sop) throws IOException { + byte[] dearmored = assumeSupported(sop::dearmor) + .data(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .getBytes(); + + byte[] dearmoredAgain = assumeSupported(sop::dearmor) + .data(dearmored) + .getBytes(); + + assertArrayEquals(dearmored, dearmoredAgain); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testArmoringTwiceIsIdempotent(SOP sop) throws IOException { + byte[] armored = ("-----BEGIN PGP SIGNATURE-----\n" + + "\n" + + "wr0EABYKAG8FgmPBdRAJEPIxVQxPR+OORxQAAAAAAB4AIHNhbHRAbm90YXRpb25z\n" + + "LnNlcXVvaWEtcGdwLm9yZ2un17fF3C46Adgzp0mU4RG8Txy/T/zOBcBw/NYaLGrQ\n" + + "FiEE64W7X6M6deFelE5j8jFVDE9H444AAMiEAP9LBQWLo4oP5IrFZPuSUQSPsUxB\n" + + "c+Qu1raXDKzS/8Q9IAD+LnHIjRHcqNPobNHXF/saXIYXeZR+LJKszTJozzwqdQE=\n" + + "=GHvQ\n" + + "-----END PGP SIGNATURE-----\n").getBytes(StandardCharsets.UTF_8); + + byte[] armoredAgain = assumeSupported(sop::armor) + .data(armored) + .getBytes(); + + JUtils.assertAsciiArmorEquals(armored, armoredAgain); + } + +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java new file mode 100644 index 0000000..855c23d --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java @@ -0,0 +1,193 @@ +// 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/ChangeKeyPasswordTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java new file mode 100644 index 0000000..a62cbb8 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +import org.junit.jupiter.api.condition.EnabledIf; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import sop.SOP; +import sop.exception.SOPGPException; +import sop.testsuite.JUtils; +import sop.testsuite.TestData; +import sop.util.UTF8Util; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class ChangeKeyPasswordTest extends AbstractSOPTest { + + static Stream provideInstances() { + return AbstractSOPTest.provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void changePasswordFromUnprotectedToProtected(SOP sop) throws IOException { + byte[] unprotectedKey = assumeSupported(sop::generateKey).generate().getBytes(); + byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + byte[] protectedKey = assumeSupported(sop::changeKeyPassword).newKeyPassphrase(password).keys(unprotectedKey).getBytes(); + + assumeSupported(sop::sign).withKeyPassword(password).key(protectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void changePasswordFromUnprotectedToUnprotected(SOP sop) throws IOException { + byte[] unprotectedKey = assumeSupported(sop::generateKey).noArmor().generate().getBytes(); + byte[] stillUnprotectedKey = assumeSupported(sop::changeKeyPassword).noArmor().keys(unprotectedKey).getBytes(); + + assertArrayEquals(unprotectedKey, stillUnprotectedKey); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void changePasswordFromProtectedToUnprotected(SOP sop) throws IOException { + byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(password).generate().getBytes(); + byte[] unprotectedKey = assumeSupported(sop::changeKeyPassword) + .oldKeyPassphrase(password) + .keys(protectedKey).getBytes(); + + assumeSupported(sop::sign).key(unprotectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void changePasswordFromProtectedToDifferentProtected(SOP sop) throws IOException { + byte[] oldPassword = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + byte[] newPassword = "0r4ng3".getBytes(UTF8Util.UTF8); + byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(oldPassword).generate().getBytes(); + byte[] reprotectedKey = assumeSupported(sop::changeKeyPassword) + .oldKeyPassphrase(oldPassword) + .newKeyPassphrase(newPassword) + .keys(protectedKey).getBytes(); + + assumeSupported(sop::sign).key(reprotectedKey).withKeyPassword(newPassword).data("Test123".getBytes(StandardCharsets.UTF_8)); + } + + + @ParameterizedTest + @MethodSource("provideInstances") + public void changePasswordWithWrongOldPasswordFails(SOP sop) throws IOException { + byte[] oldPassword = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + byte[] newPassword = "monkey123".getBytes(UTF8Util.UTF8); + byte[] wrongPassword = "0r4ng3".getBytes(UTF8Util.UTF8); + + byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(oldPassword).generate().getBytes(); + assertThrows(SOPGPException.KeyIsProtected.class, () -> assumeSupported(sop::changeKeyPassword) + .oldKeyPassphrase(wrongPassword) + .newKeyPassphrase(newPassword) + .keys(protectedKey).getBytes()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void nonUtf8PasswordsFail(SOP sop) { + assertThrows(SOPGPException.PasswordNotHumanReadable.class, () -> + assumeSupported(sop::changeKeyPassword).oldKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); + assertThrows(SOPGPException.PasswordNotHumanReadable.class, () -> + assumeSupported(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-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java new file mode 100644 index 0000000..8fd201a --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +import org.junit.jupiter.api.Assertions; +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.ByteArrayAndResult; +import sop.DecryptionResult; +import sop.SOP; +import sop.SessionKey; +import sop.testsuite.TestData; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class DecryptWithSessionKeyTest extends AbstractSOPTest { + + private static final String CIPHERTEXT = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wV4DR2b2udXyHrYSAQdAy+Et2hCh4ubh8KsmM8ctRDN6Pee+UHVVcI6YXpY9S2cw\n" + + "1QEROCgfm6xGb+hgxmoFrWhtZU03Arb27ZmpWA6e6Ha9jFdB4/DDbqbhlVuFOmti\n" + + "0j8BqGjEvEYAon+8F9TwMaDbPjjy9SdgQBorlM88ChIW14KQtpG9FZN+r+xVKPG1\n" + + "8EIOxI4qOZaH3Wejraca31M=\n" + + "=1imC\n" + + "-----END PGP MESSAGE-----\n"; + private static final String SESSION_KEY = "9:ED682800F5FEA829A82E8B7DDF8CE9CF4BF9BB45024B017764462EE53101C36A"; + + static Stream provideInstances() { + return provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testDecryptAndExtractSessionKey(SOP sop) throws IOException { + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) + .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .ciphertext(CIPHERTEXT.getBytes(StandardCharsets.UTF_8)) + .toByteArrayAndResult(); + + assertEquals(SESSION_KEY, bytesAndResult.getResult().getSessionKey().get().toString()); + + Assertions.assertArrayEquals(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8), bytesAndResult.getBytes()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testDecryptWithSessionKey(SOP sop) throws IOException { + byte[] decrypted = assumeSupported(sop::decrypt) + .withSessionKey(SessionKey.fromString(SESSION_KEY)) + .ciphertext(CIPHERTEXT.getBytes(StandardCharsets.UTF_8)) + .toByteArrayAndResult() + .getBytes(); + + Assertions.assertArrayEquals(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8), decrypted); + } +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java new file mode 100644 index 0000000..415b9db --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java @@ -0,0 +1,312 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +import org.junit.jupiter.api.condition.EnabledIf; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import sop.SOP; +import sop.Verification; +import sop.enums.SignAs; +import sop.enums.SignatureMode; +import sop.exception.SOPGPException; +import sop.testsuite.JUtils; +import sop.testsuite.TestData; +import sop.testsuite.assertions.VerificationListAssert; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { + + static Stream provideInstances() { + return provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void signVerifyWithAliceKey(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + byte[] signature = assumeSupported(sop::detachedSign) + .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .data(message) + .toByteArrayAndResult() + .getBytes(); + + List verificationList = assumeSupported(sop::detachedVerify) + .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .signatures(signature) + .data(message); + + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT) + .hasModeOrNull(SignatureMode.binary); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void signVerifyTextModeWithAliceKey(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + byte[] signature = assumeSupported(sop::detachedSign) + .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .mode(SignAs.text) + .data(message) + .toByteArrayAndResult() + .getBytes(); + + List verificationList = assumeSupported(sop::detachedVerify) + .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .signatures(signature) + .data(message); + + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT) + .hasModeOrNull(SignatureMode.text); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void verifyKnownMessageWithAliceCert(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + byte[] signature = TestData.ALICE_DETACHED_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8); + + List verificationList = assumeSupported(sop::detachedVerify) + .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .signatures(signature) + .data(message); + + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void signVerifyWithBobKey(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + byte[] signature = assumeSupported(sop::detachedSign) + .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) + .data(message) + .toByteArrayAndResult() + .getBytes(); + + List verificationList = assumeSupported(sop::detachedVerify) + .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) + .signatures(signature) + .data(message); + + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void signVerifyWithCarolKey(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + byte[] signature = assumeSupported(sop::detachedSign) + .key(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8)) + .data(message) + .toByteArrayAndResult() + .getBytes(); + + List verificationList = assumeSupported(sop::detachedVerify) + .cert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8)) + .signatures(signature) + .data(message); + + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.CAROL_SIGNING_FINGERPRINT, TestData.CAROL_PRIMARY_FINGERPRINT); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void signVerifyWithEncryptedKey(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + byte[] signature = assumeSupported(sop::detachedSign) + .key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8)) + .withKeyPassword(TestData.PASSWORD) + .data(message) + .toByteArrayAndResult() + .getBytes(); + + JUtils.assertArrayStartsWith(signature, TestData.BEGIN_PGP_SIGNATURE); + + List verificationList = assumeSupported(sop::detachedVerify) + .cert(TestData.PASSWORD_PROTECTED_CERT.getBytes(StandardCharsets.UTF_8)) + .signatures(signature) + .data(message); + + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.PASSWORD_PROTECTED_SIGNING_FINGERPRINT, TestData.PASSWORD_PROTECTED_PRIMARY_FINGERPRINT); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void signArmorVerifyWithBobKey(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + byte[] signature = assumeSupported(sop::detachedSign) + .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) + .noArmor() + .data(message) + .toByteArrayAndResult() + .getBytes(); + + byte[] armored = assumeSupported(sop::armor) + .data(signature) + .getBytes(); + + List verificationList = assumeSupported(sop::detachedVerify) + .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) + .signatures(armored) + .data(message); + + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void verifyNotAfterThrowsNoSignature(SOP sop) { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + 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) + .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .notAfter(beforeSignature) + .signatures(signature) + .data(message)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void verifyNotBeforeThrowsNoSignature(SOP sop) { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + 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) + .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .notBefore(afterSignature) + .signatures(signature) + .data(message)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void signWithAliceVerifyWithBobThrowsNoSignature(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + byte[] signatures = assumeSupported(sop::detachedSign) + .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .data(message) + .toByteArrayAndResult() + .getBytes(); + + assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::detachedVerify) + .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) + .signatures(signatures) + .data(message)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void signVerifyWithEncryptedKeyWithoutPassphraseFails(SOP sop) { + assertThrows(SOPGPException.KeyIsProtected.class, () -> + assumeSupported(sop::detachedSign) + .key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8)) + .data(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) + .toByteArrayAndResult() + .getBytes()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void signWithProtectedKeyAndMultiplePassphrasesTest(SOP sop) + throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + byte[] signature = assumeSupported(sop::sign) + .key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8)) + .withKeyPassword("wrong") + .withKeyPassword(TestData.PASSWORD) // correct + .withKeyPassword("wrong2") + .data(message) + .toByteArrayAndResult() + .getBytes(); + + List verificationList = assumeSupported(sop::verify) + .cert(TestData.PASSWORD_PROTECTED_CERT.getBytes(StandardCharsets.UTF_8)) + .signatures(signature) + .data(message); + + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.PASSWORD_PROTECTED_SIGNING_FINGERPRINT, TestData.PASSWORD_PROTECTED_PRIMARY_FINGERPRINT); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void verifyMissingCertCausesMissingArg(SOP sop) { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + assertThrows(SOPGPException.MissingArg.class, () -> + assumeSupported(sop::verify) + .signatures(TestData.ALICE_DETACHED_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8)) + .data(message)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void signVerifyWithMultipleKeys(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + byte[] signatures = assumeSupported(sop::detachedSign) + .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) + .data(message) + .toByteArrayAndResult() + .getBytes(); + + List verificationList = assumeSupported(sop::detachedVerify) + .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) + .signatures(signatures) + .data(message); + + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .sizeEquals(2) + .containsVerificationBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT) + .containsVerificationBy(TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT); + } + + +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java new file mode 100644 index 0000000..937b5b7 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java @@ -0,0 +1,401 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +import org.junit.jupiter.api.condition.EnabledIf; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import sop.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; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class EncryptDecryptTest extends AbstractSOPTest { + + static Stream provideInstances() { + return provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void encryptDecryptRoundTripPasswordTest(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + ByteArrayAndResult encResult = assumeSupported(sop::encrypt) + .withPassword("sw0rdf1sh") + .plaintext(message) + .toByteArrayAndResult(); + + byte[] ciphertext = encResult.getBytes(); + Optional encSessionKey = encResult.getResult().getSessionKey(); + + ByteArrayAndResult decResult = assumeSupported(sop::decrypt) + .withPassword("sw0rdf1sh") + .ciphertext(ciphertext) + .toByteArrayAndResult(); + + 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."); + } + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void encryptDecryptRoundTripAliceTest(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + byte[] ciphertext = assumeSupported(sop::encrypt) + .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .plaintext(message) + .toByteArrayAndResult() + .getBytes(); + + ByteArrayAndResult bytesAndResult = assumeSupported(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."); + + DecryptionResult result = bytesAndResult.getResult(); + if (result.getSessionKey().isPresent()) { + assertNotNull(result.getSessionKey().get(), "Session key MUST NOT be null."); + } + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void encryptDecryptRoundTripBobTest(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + byte[] ciphertext = assumeSupported(sop::encrypt) + .withCert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) + .plaintext(message) + .toByteArrayAndResult() + .getBytes(); + + byte[] plaintext = assumeSupported(sop::decrypt) + .withKey(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) + .ciphertext(ciphertext) + .toByteArrayAndResult() + .getBytes(); + + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void encryptDecryptRoundTripCarolTest(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + byte[] ciphertext = assumeSupported(sop::encrypt) + .withCert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8)) + .plaintext(message) + .toByteArrayAndResult() + .getBytes(); + + byte[] plaintext = assumeSupported(sop::decrypt) + .withKey(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8)) + .ciphertext(ciphertext) + .toByteArrayAndResult() + .getBytes(); + + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void encryptNoArmorThenArmorThenDecryptRoundTrip(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + byte[] ciphertext = assumeSupported(sop::encrypt) + .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .noArmor() + .plaintext(message) + .toByteArrayAndResult() + .getBytes(); + + byte[] armored = assumeSupported(sop::armor) + .data(ciphertext) + .getBytes(); + + ByteArrayAndResult bytesAndResult = assumeSupported(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."); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void encryptSignDecryptVerifyRoundTripAliceTest(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + byte[] ciphertext = assumeSupported(sop::encrypt) + .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .mode(EncryptAs.binary) + .plaintext(message) + .toByteArrayAndResult() + .getBytes(); + + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) + .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .ciphertext(ciphertext) + .toByteArrayAndResult(); + + byte[] plaintext = bytesAndResult.getBytes(); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); + + DecryptionResult result = bytesAndResult.getResult(); + if (result.getSessionKey().isPresent()) { + assertNotNull(result.getSessionKey().get(), "Session key MUST NOT be null."); + } + + List verificationList = result.getVerifications(); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT) + .hasModeOrNull(SignatureMode.binary); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void encryptSignAsTextDecryptVerifyRoundTripAliceTest(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + byte[] ciphertext = assumeSupported(sop::encrypt) + .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .mode(EncryptAs.text) + .plaintext(message) + .toByteArrayAndResult() + .getBytes(); + + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) + .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .ciphertext(ciphertext) + .toByteArrayAndResult(); + + byte[] plaintext = bytesAndResult.getBytes(); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); + + DecryptionResult result = bytesAndResult.getResult(); + assertNotNull(result.getSessionKey().get()); + + List verificationList = result.getVerifications(); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT) + .hasModeOrNull(SignatureMode.text); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void encryptSignDecryptVerifyRoundTripWithFreshEncryptedKeyTest(SOP sop) throws IOException { + byte[] keyPassword = "sw0rdf1sh".getBytes(StandardCharsets.UTF_8); + byte[] key = assumeSupported(sop::generateKey) + .withKeyPassword(keyPassword) + .userId("Alice ") + .generate() + .getBytes(); + byte[] cert = assumeSupported(sop::extractCert) + .key(key) + .getBytes(); + + byte[] message = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); + byte[] ciphertext = assumeSupported(sop::encrypt) + .withCert(cert) + .signWith(key) + .withKeyPassword(keyPassword) + .plaintext(message) + .toByteArrayAndResult() + .getBytes(); + + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) + .withKey(key) + .withKeyPassword(keyPassword) + .verifyWithCert(cert) + .ciphertext(ciphertext) + .toByteArrayAndResult(); + + List verifications = bytesAndResult.getResult().getVerifications(); + VerificationListAssert.assertThatVerificationList(verifications) + .isNotEmpty() + .hasSingleItem(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void decryptVerifyNotAfterTest(SOP sop) throws ParseException { + byte[] message = ("-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wV4DR2b2udXyHrYSAQdAwlOwwyxFDJta5+H9abgSj8jum9v7etUc9usdrElESmow\n" + + "2Hka48AFVfOezYh0OFn9R8+DMcpuE+e4nw3XnnX5nKs/j3AC2IW6zRHUkRcF3ZCq\n" + + "0sBNAfjnTYCMjuBmqdcCLzaZT4Hadnpg6neP1UecT/jP14maGfv8nwt0IDGR0Bik\n" + + "0WC/UJLpWyJ/6TgRrA5hNfANVnfiFBzIiThiVBRWPT2StHr2cOAvFxQK4Uk07rK9\n" + + "9aTUak8FpML+QA83U8I3qOk4QbzGVBP+IDJ+AKmvDz+0V+9kUhKp+8vyXsBmo9c3\n" + + "SAXjhFSiPQkU7ORsc6gQHL9+KPOU+W2poPK87H3cmaGiusnXMeLXLIUbkBUJTswd\n" + + "JNrA2yAkTTFP9QabsdcdTGoeYamq1c29kHF3GOTTcEqXw4WWXngcF7Kbcf435kkL\n" + + "4iSJnCaxTPftKUxmiGqMqLef7ICVnq/lz3HrH1VD54s=\n" + + "=Ebi3\n" + + "-----END PGP MESSAGE-----").getBytes(StandardCharsets.UTF_8); + Date signatureDate = UTCUtil.parseUTCDate("2023-01-13T16:09:32Z"); + + Date beforeSignature = new Date(signatureDate.getTime() - 1000); // 1 sec before signing date + + assertThrows(SOPGPException.NoSignature.class, () -> { + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) + .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .verifyNotAfter(beforeSignature) + .ciphertext(message) + .toByteArrayAndResult(); + + // Some implementations do not throw NoSignature and instead return an empty list. + if (bytesAndResult.getResult().getVerifications().isEmpty()) { + throw new SOPGPException.NoSignature("No verifiable signature found."); + } + }); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void decryptVerifyNotBeforeTest(SOP sop) throws ParseException { + byte[] message = ("-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wV4DR2b2udXyHrYSAQdAwlOwwyxFDJta5+H9abgSj8jum9v7etUc9usdrElESmow\n" + + "2Hka48AFVfOezYh0OFn9R8+DMcpuE+e4nw3XnnX5nKs/j3AC2IW6zRHUkRcF3ZCq\n" + + "0sBNAfjnTYCMjuBmqdcCLzaZT4Hadnpg6neP1UecT/jP14maGfv8nwt0IDGR0Bik\n" + + "0WC/UJLpWyJ/6TgRrA5hNfANVnfiFBzIiThiVBRWPT2StHr2cOAvFxQK4Uk07rK9\n" + + "9aTUak8FpML+QA83U8I3qOk4QbzGVBP+IDJ+AKmvDz+0V+9kUhKp+8vyXsBmo9c3\n" + + "SAXjhFSiPQkU7ORsc6gQHL9+KPOU+W2poPK87H3cmaGiusnXMeLXLIUbkBUJTswd\n" + + "JNrA2yAkTTFP9QabsdcdTGoeYamq1c29kHF3GOTTcEqXw4WWXngcF7Kbcf435kkL\n" + + "4iSJnCaxTPftKUxmiGqMqLef7ICVnq/lz3HrH1VD54s=\n" + + "=Ebi3\n" + + "-----END PGP MESSAGE-----").getBytes(StandardCharsets.UTF_8); + Date signatureDate = UTCUtil.parseUTCDate("2023-01-13T16:09:32Z"); + + Date afterSignature = new Date(signatureDate.getTime() + 1000); // 1 sec after signing date + + assertThrows(SOPGPException.NoSignature.class, () -> { + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) + .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .verifyNotBefore(afterSignature) + .ciphertext(message) + .toByteArrayAndResult(); + + // Some implementations do not throw NoSignature and instead return an empty list. + if (bytesAndResult.getResult().getVerifications().isEmpty()) { + throw new SOPGPException.NoSignature("No verifiable signature found."); + } + }); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void missingArgsTest(SOP sop) { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + assertThrows(SOPGPException.MissingArg.class, () -> assumeSupported(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-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java new file mode 100644 index 0000000..94d9927 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +import org.junit.jupiter.api.Assertions; +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.testsuite.JUtils; +import sop.testsuite.TestData; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class ExtractCertTest extends AbstractSOPTest { + + static Stream provideInstances() { + return provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void extractArmoredCertFromArmoredKeyTest(SOP sop) throws IOException { + InputStream keyIn = assumeSupported(sop::generateKey) + .userId("Alice ") + .generate() + .getInputStream(); + + byte[] cert = assumeSupported(sop::extractCert).key(keyIn).getBytes(); + JUtils.assertArrayStartsWith(cert, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK); + JUtils.assertArrayEndsWithIgnoreNewlines(cert, TestData.END_PGP_PUBLIC_KEY_BLOCK); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void extractAliceCertFromAliceKeyTest(SOP sop) throws IOException { + byte[] armoredCert = assumeSupported(sop::extractCert) + .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .getBytes(); + JUtils.assertAsciiArmorEquals(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8), armoredCert); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void extractBobsCertFromBobsKeyTest(SOP sop) throws IOException { + byte[] armoredCert = assumeSupported(sop::extractCert) + .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) + .getBytes(); + JUtils.assertAsciiArmorEquals(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8), armoredCert); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void extractCarolsCertFromCarolsKeyTest(SOP sop) throws IOException { + byte[] armoredCert = assumeSupported(sop::extractCert) + .key(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8)) + .getBytes(); + JUtils.assertAsciiArmorEquals(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8), armoredCert); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void extractUnarmoredCertFromArmoredKeyTest(SOP sop) throws IOException { + InputStream keyIn = assumeSupported(sop::generateKey) + .userId("Alice ") + .generate() + .getInputStream(); + + byte[] cert = assumeSupported(sop::extractCert) + .noArmor() + .key(keyIn) + .getBytes(); + + Assertions.assertFalse(JUtils.arrayStartsWith(cert, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void extractArmoredCertFromUnarmoredKeyTest(SOP sop) throws IOException { + InputStream keyIn = assumeSupported(sop::generateKey) + .userId("Alice ") + .noArmor() + .generate() + .getInputStream(); + + byte[] cert = assumeSupported(sop::extractCert) + .key(keyIn) + .getBytes(); + + JUtils.assertArrayStartsWith(cert, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK); + JUtils.assertArrayEndsWithIgnoreNewlines(cert, TestData.END_PGP_PUBLIC_KEY_BLOCK); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void extractUnarmoredCertFromUnarmoredKeyTest(SOP sop) throws IOException { + InputStream keyIn = assumeSupported(sop::generateKey) + .noArmor() + .userId("Alice ") + .generate() + .getInputStream(); + + byte[] cert = assumeSupported(sop::extractCert) + .noArmor() + .key(keyIn) + .getBytes(); + + Assertions.assertFalse(JUtils.arrayStartsWith(cert, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); + } +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java new file mode 100644 index 0000000..787cf62 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +import org.junit.jupiter.api.Assertions; +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; +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 { + + static Stream provideInstances() { + return provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void generateKeyTest(SOP sop) throws IOException { + byte[] key = assumeSupported(sop::generateKey) + .userId("Alice ") + .generate() + .getBytes(); + + JUtils.assertArrayStartsWith(key, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); + JUtils.assertArrayEndsWithIgnoreNewlines(key, TestData.END_PGP_PRIVATE_KEY_BLOCK); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void generateKeyNoArmor(SOP sop) throws IOException { + byte[] key = assumeSupported(sop::generateKey) + .userId("Alice ") + .noArmor() + .generate() + .getBytes(); + + Assertions.assertFalse(JUtils.arrayStartsWith(key, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void generateKeyWithMultipleUserIdsTest(SOP sop) throws IOException { + byte[] key = assumeSupported(sop::generateKey) + .userId("Alice ") + .userId("Bob ") + .generate() + .getBytes(); + + JUtils.assertArrayStartsWith(key, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); + JUtils.assertArrayEndsWithIgnoreNewlines(key, TestData.END_PGP_PRIVATE_KEY_BLOCK); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void generateKeyWithoutUserIdTest(SOP sop) throws IOException { + byte[] key = assumeSupported(sop::generateKey) + .generate() + .getBytes(); + + JUtils.assertArrayStartsWith(key, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); + JUtils.assertArrayEndsWithIgnoreNewlines(key, TestData.END_PGP_PRIVATE_KEY_BLOCK); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void generateKeyWithPasswordTest(SOP sop) throws IOException { + byte[] key = assumeSupported(sop::generateKey) + .userId("Alice ") + .withKeyPassword("sw0rdf1sh") + .generate() + .getBytes(); + + JUtils.assertArrayStartsWith(key, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); + JUtils.assertArrayEndsWithIgnoreNewlines(key, TestData.END_PGP_PRIVATE_KEY_BLOCK); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void generateKeyWithMultipleUserIdsAndPassword(SOP sop) throws IOException { + byte[] key = assumeSupported(sop::generateKey) + .userId("Alice ") + .userId("Bob ") + .withKeyPassword("sw0rdf1sh") + .generate() + .getBytes(); + + JUtils.assertArrayStartsWith(key, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); + JUtils.assertArrayEndsWithIgnoreNewlines(key, TestData.END_PGP_PRIVATE_KEY_BLOCK); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void generateSigningOnlyKey(SOP sop) throws IOException { + byte[] signingOnlyKey = assumeSupported(sop::generateKey) + .signingOnly() + .userId("Alice ") + .generate() + .getBytes(); + byte[] signingOnlyCert = assumeSupported(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); + } +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java new file mode 100644 index 0000000..ac043b3 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +import org.junit.jupiter.api.Assertions; +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.ByteArrayAndResult; +import sop.SOP; +import sop.Signatures; +import sop.Verification; +import sop.testsuite.JUtils; +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.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class InlineSignInlineDetachDetachedVerifyTest extends AbstractSOPTest { + + static Stream provideInstances() { + return provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void inlineSignThenDetachThenDetachedVerifyTest(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + byte[] inlineSigned = assumeSupported(sop::inlineSign) + .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .data(message) + .getBytes(); + + ByteArrayAndResult bytesAndResult = assumeSupported(sop::inlineDetach) + .message(inlineSigned) + .toByteArrayAndResult(); + + byte[] plaintext = bytesAndResult.getBytes(); + assertArrayEquals(message, plaintext); + + byte[] signatures = bytesAndResult.getResult() + .getBytes(); + + List verifications = assumeSupported(sop::detachedVerify) + .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .signatures(signatures) + .data(plaintext); + + assertFalse(verifications.isEmpty()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void inlineSignThenDetachNoArmorThenArmorThenDetachedVerifyTest(SOP sop) throws IOException { + byte[] message = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); + + byte[] inlineSigned = assumeSupported(sop::inlineSign) + .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .data(message) + .getBytes(); + + ByteArrayAndResult bytesAndResult = assumeSupported(sop::inlineDetach) + .noArmor() + .message(inlineSigned) + .toByteArrayAndResult(); + + byte[] plaintext = bytesAndResult.getBytes(); + assertArrayEquals(message, plaintext); + + byte[] signatures = bytesAndResult.getResult() + .getBytes(); + Assertions.assertFalse(JUtils.arrayStartsWith(signatures, TestData.BEGIN_PGP_SIGNATURE)); + + byte[] armored = assumeSupported(sop::armor) + .data(signatures) + .getBytes(); + JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_SIGNATURE); + + List verifications = assumeSupported(sop::detachedVerify) + .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .signatures(armored) + .data(plaintext); + + assertFalse(verifications.isEmpty()); + } +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java new file mode 100644 index 0000000..d751ee8 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java @@ -0,0 +1,241 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +import org.junit.jupiter.api.Assertions; +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.ByteArrayAndResult; +import sop.SOP; +import sop.Verification; +import sop.enums.InlineSignAs; +import sop.enums.SignatureMode; +import sop.exception.SOPGPException; +import sop.testsuite.JUtils; +import sop.testsuite.TestData; +import sop.testsuite.assertions.VerificationListAssert; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +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.assertThrows; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class InlineSignInlineVerifyTest extends AbstractSOPTest { + + static Stream provideInstances() { + return provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void inlineSignVerifyAlice(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + byte[] inlineSigned = assumeSupported(sop::inlineSign) + .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .data(message) + .getBytes(); + + JUtils.assertArrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE); + + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) + .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .data(inlineSigned) + .toByteArrayAndResult(); + + assertArrayEquals(message, bytesAndResult.getBytes()); + + List verificationList = bytesAndResult.getResult(); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void inlineSignVerifyAliceNoArmor(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + byte[] inlineSigned = assumeSupported(sop::inlineSign) + .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .noArmor() + .data(message) + .getBytes(); + + Assertions.assertFalse(JUtils.arrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE)); + + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) + .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .data(inlineSigned) + .toByteArrayAndResult(); + + assertArrayEquals(message, bytesAndResult.getBytes()); + + List verificationList = bytesAndResult.getResult(); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void clearsignVerifyAlice(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + byte[] clearsigned = assumeSupported(sop::inlineSign) + .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) + .mode(InlineSignAs.clearsigned) + .data(message) + .getBytes(); + + JUtils.assertArrayStartsWith(clearsigned, TestData.BEGIN_PGP_SIGNED_MESSAGE); + + ByteArrayAndResult> bytesAndResult = assumeSupported(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."); + + List verificationList = bytesAndResult.getResult(); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT) + .hasModeOrNull(SignatureMode.text); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void inlineVerifyCompareSignatureDate(SOP sop) throws IOException { + byte[] message = TestData.ALICE_INLINE_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8); + Date signatureDate = TestData.ALICE_INLINE_SIGNED_MESSAGE_DATE; + + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) + .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .data(message) + .toByteArrayAndResult(); + + List verificationList = bytesAndResult.getResult(); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .isCreatedAt(signatureDate) + .issuedBy(TestData.ALICE_SIGNING_FINGERPRINT, TestData.ALICE_PRIMARY_FINGERPRINT); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void assertNotBeforeThrowsNoSignature(SOP sop) { + byte[] message = TestData.ALICE_INLINE_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8); + 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) + .notBefore(afterSignature) + .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .data(message) + .toByteArrayAndResult()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void assertNotAfterThrowsNoSignature(SOP sop) { + byte[] message = TestData.ALICE_INLINE_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8); + 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) + .notAfter(beforeSignature) + .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) + .data(message) + .toByteArrayAndResult()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void inlineSignVerifyBob(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + byte[] inlineSigned = assumeSupported(sop::inlineSign) + .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) + .data(message) + .getBytes(); + + JUtils.assertArrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE); + + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) + .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) + .data(inlineSigned) + .toByteArrayAndResult(); + + assertArrayEquals(message, bytesAndResult.getBytes()); + + List verificationList = bytesAndResult.getResult(); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.BOB_SIGNING_FINGERPRINT, TestData.BOB_PRIMARY_FINGERPRINT); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void inlineSignVerifyCarol(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + byte[] inlineSigned = assumeSupported(sop::inlineSign) + .key(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8)) + .data(message) + .getBytes(); + + JUtils.assertArrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE); + + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) + .cert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8)) + .data(inlineSigned) + .toByteArrayAndResult(); + + assertArrayEquals(message, bytesAndResult.getBytes()); + + List verificationList = bytesAndResult.getResult(); + VerificationListAssert.assertThatVerificationList(verificationList) + .isNotEmpty() + .hasSingleItem() + .issuedBy(TestData.CAROL_SIGNING_FINGERPRINT, TestData.CAROL_PRIMARY_FINGERPRINT); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void inlineSignVerifyProtectedKey(SOP sop) throws IOException { + byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + + byte[] inlineSigned = assumeSupported(sop::inlineSign) + .withKeyPassword(TestData.PASSWORD) + .key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8)) + .mode(InlineSignAs.binary) + .data(message) + .getBytes(); + + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) + .cert(TestData.PASSWORD_PROTECTED_CERT.getBytes(StandardCharsets.UTF_8)) + .data(inlineSigned) + .toByteArrayAndResult(); + + List verificationList = bytesAndResult.getResult(); + VerificationListAssert.assertThatVerificationList(verificationList) + .hasSingleItem() + .issuedBy(TestData.PASSWORD_PROTECTED_SIGNING_FINGERPRINT, TestData.PASSWORD_PROTECTED_PRIMARY_FINGERPRINT); + } + +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java new file mode 100644 index 0000000..4faa1b3 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import sop.Profile; +import sop.SOP; +import sop.exception.SOPGPException; + +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ListProfilesTest extends AbstractSOPTest { + + static Stream provideInstances() { + return provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void listGenerateKeyProfiles(SOP sop) { + List profiles = assumeSupported(sop::listProfiles) + .generateKey(); + + assertFalse(profiles.isEmpty()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void listEncryptProfiles(SOP sop) { + List profiles = assumeSupported(sop::listProfiles) + .encrypt(); + + assertFalse(profiles.isEmpty()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void listUnsupportedProfiles(SOP sop) { + assertThrows(SOPGPException.UnsupportedProfile.class, () -> assumeSupported(sop::listProfiles) + .subcommand("invalid")); + } +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java new file mode 100644 index 0000000..501f53c --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java @@ -0,0 +1,164 @@ +// 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-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java new file mode 100644 index 0000000..1880d58 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +import org.junit.jupiter.api.condition.EnabledIf; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import sop.SOP; +import sop.Verification; +import sop.exception.SOPGPException; +import sop.testsuite.JUtils; +import sop.testsuite.TestData; +import sop.testsuite.assertions.VerificationListAssert; +import sop.util.UTF8Util; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class RevokeKeyTest extends AbstractSOPTest { + + static Stream provideInstances() { + return provideBackends(); + } + + @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(); + + 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(); + + assertFalse(JUtils.arrayStartsWith(revocation, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); + assertFalse(Arrays.equals(secretKey, revocation)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void revokeCertificateFails(SOP sop) throws IOException { + byte[] secretKey = assumeSupported(sop::generateKey).generate().getBytes(); + byte[] certificate = assumeSupported(sop::extractCert).key(secretKey).getBytes(); + + assertThrows(SOPGPException.BadData.class, () -> assumeSupported(sop::revokeKey).keys(certificate).getBytes()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void revokeProtectedKey(SOP sop) throws IOException { + byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice ").generate().getBytes(); + byte[] revocation = assumeSupported(sop::revokeKey).withKeyPassword(password).keys(secretKey).getBytes(); + + assertFalse(Arrays.equals(secretKey, revocation)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void revokeProtectedKeyWithMultiplePasswordOptions(SOP sop) throws IOException { + byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + byte[] wrongPassword = "0r4ng3".getBytes(UTF8Util.UTF8); + byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice ").generate().getBytes(); + byte[] revocation = assumeSupported(sop::revokeKey).withKeyPassword(wrongPassword).withKeyPassword(password).keys(secretKey).getBytes(); + + assertFalse(Arrays.equals(secretKey, revocation)); + } + + @ParameterizedTest + @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(); + + assertThrows(SOPGPException.KeyIsProtected.class, () -> assumeSupported(sop::revokeKey).keys(secretKey).getBytes()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void revokeProtectedKeyWithWrongPassphraseFails(SOP sop) throws IOException { + byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + String wrongPassword = "or4ng3"; + byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice ").generate().getBytes(); + + assertThrows(SOPGPException.KeyIsProtected.class, () -> assumeSupported(sop::revokeKey).withKeyPassword(wrongPassword).keys(secretKey).getBytes()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void revokeKeyIsNowHardRevoked(SOP sop) throws IOException { + byte[] key = assumeSupported(sop::generateKey).generate().getBytes(); + byte[] cert = assumeSupported(sop::extractCert).key(key).getBytes(); + + // Sign a message with the key + byte[] msg = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); + byte[] signedMsg = assumeSupported(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(); + 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()); + } +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java new file mode 100644 index 0000000..71f7efd --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +import org.junit.jupiter.api.condition.EnabledIf; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import 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; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class VersionTest extends AbstractSOPTest { + + static Stream provideInstances() { + return provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void versionNameTest(SOP sop) { + String name = assumeSupported(sop::version).getName(); + assertNotNull(name); + assertFalse(name.isEmpty()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void versionVersionTest(SOP sop) { + String version = assumeSupported(sop::version).getVersion(); + assertFalse(version.isEmpty()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void backendVersionTest(SOP sop) { + String backend = assumeSupported(sop::version).getBackendVersion(); + assertFalse(backend.isEmpty()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void extendedVersionTest(SOP sop) { + String extended = assumeSupported(sop::version).getExtendedVersion(); + assertFalse(extended.isEmpty()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void sopSpecVersionTest(SOP sop) { + try { + assumeSupported(sop::version).getSopSpecVersion(); + } catch (RuntimeException e) { + throw new TestAbortedException("SOP backend does not support 'version --sop-spec' yet."); + } + + String sopSpec = assumeSupported(sop::version).getSopSpecVersion(); + if (assumeSupported(sop::version).isSopSpecImplementationIncomplete()) { + assertTrue(sopSpec.startsWith("~draft-dkg-openpgp-stateless-cli-")); + } else { + assertTrue(sopSpec.startsWith("draft-dkg-openpgp-stateless-cli-")); + } + + int sopRevision = 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()); + } +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/package-info.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/package-info.java new file mode 100644 index 0000000..e3a27ee --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * SOP binary test suite. + */ +package sop.testsuite.operation; diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/package-info.java b/sop-java-testfixtures/src/main/java/sop/testsuite/package-info.java new file mode 100644 index 0000000..cfb68d2 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * SOP binary test suite. + */ +package sop.testsuite; diff --git a/sop-java/README.md b/sop-java/README.md index 471d510..507ac5b 100644 --- a/sop-java/README.md +++ b/sop-java/README.md @@ -55,16 +55,13 @@ List signatureVerifications = messageInfo.getVerifications(); Furthermore, the API is capable of signing messages and verifying unencrypted signed data, as well as adding and removing ASCII armor. -### Limitations -As per the spec, sop-java does not (yet) deal with encrypted OpenPGP keys. - ## Why should I use this? If you need to use OpenPGP functionality like encrypting/decrypting messages, or creating/verifying signatures inside your application, you probably don't want to start from scratch and instead reuse some library. Instead of locking yourselves in by depending hard on that one library, you can simply depend on the interfaces from -`sop-java` and plug in a library (such as `pgpainless-sop`) that implements said interfaces. +`sop-java` and plug in a library (such as `pgpainless-sop`, `external-sop`) that implements said interfaces. That way you don't make yourself dependent from a single OpenPGP library and stay flexible. Should another library emerge, that better suits your needs (and implements `sop-java`), you can easily switch diff --git a/sop-java/build.gradle b/sop-java/build.gradle index ff0f293..c6f4e4e 100644 --- a/sop-java/build.gradle +++ b/sop-java/build.gradle @@ -1,7 +1,10 @@ +import org.apache.tools.ant.filters.ReplaceTokens + // SPDX-FileCopyrightText: 2021 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 +import org.apache.tools.ant.filters.* plugins { id 'java-library' } @@ -14,9 +17,21 @@ repositories { dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + testImplementation(project(":sop-java-testfixtures")) + + // @Nullable, @Nonnull annotations + implementation "com.google.code.findbugs:jsr305:3.0.2" + +} + +processResources { + filter ReplaceTokens, tokens: [ + "project.version": project.version.toString() + ] } test { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/sop-java/src/main/java/sop/ByteArrayAndResult.java b/sop-java/src/main/java/sop/ByteArrayAndResult.java deleted file mode 100644 index fd2b39a..0000000 --- a/sop-java/src/main/java/sop/ByteArrayAndResult.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; - -/** - * Tuple of a byte array and associated result object. - * @param type of result - */ -public class ByteArrayAndResult { - - private final byte[] bytes; - private final T result; - - public ByteArrayAndResult(byte[] bytes, T result) { - this.bytes = bytes; - this.result = result; - } - - /** - * Return the byte array part. - * - * @return bytes - */ - public byte[] getBytes() { - return bytes; - } - - /** - * Return the result part. - * - * @return result - */ - public T getResult() { - return result; - } - - /** - * Return the byte array part as an {@link InputStream}. - * - * @return input stream - */ - public InputStream getInputStream() { - return new ByteArrayInputStream(getBytes()); - } -} diff --git a/sop-java/src/main/java/sop/DecryptionResult.java b/sop-java/src/main/java/sop/DecryptionResult.java deleted file mode 100644 index 4f0e1ab..0000000 --- a/sop-java/src/main/java/sop/DecryptionResult.java +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import sop.util.Optional; - -public class DecryptionResult { - - private final Optional sessionKey; - private final List verifications; - - public DecryptionResult(SessionKey sessionKey, List verifications) { - this.sessionKey = Optional.ofNullable(sessionKey); - this.verifications = Collections.unmodifiableList(verifications); - } - - public Optional getSessionKey() { - return sessionKey; - } - - public List getVerifications() { - return new ArrayList<>(verifications); - } -} diff --git a/sop-java/src/main/java/sop/MicAlg.java b/sop-java/src/main/java/sop/MicAlg.java deleted file mode 100644 index 5bee787..0000000 --- a/sop-java/src/main/java/sop/MicAlg.java +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.OutputStream; -import java.io.PrintWriter; - -public class MicAlg { - - private final String micAlg; - - public MicAlg(String micAlg) { - if (micAlg == null) { - throw new IllegalArgumentException("MicAlg String cannot be null."); - } - this.micAlg = micAlg; - } - - public static MicAlg empty() { - return new MicAlg(""); - } - - public static MicAlg fromHashAlgorithmId(int id) { - switch (id) { - case 1: - return new MicAlg("pgp-md5"); - case 2: - return new MicAlg("pgp-sha1"); - case 3: - return new MicAlg("pgp-ripemd160"); - case 8: - return new MicAlg("pgp-sha256"); - case 9: - return new MicAlg("pgp-sha384"); - case 10: - return new MicAlg("pgp-sha512"); - case 11: - return new MicAlg("pgp-sha224"); - default: - throw new IllegalArgumentException("Unsupported hash algorithm ID: " + id); - } - } - - public String getMicAlg() { - return micAlg; - } - - public void writeTo(OutputStream outputStream) { - PrintWriter pw = new PrintWriter(outputStream); - pw.write(getMicAlg()); - pw.close(); - } -} diff --git a/sop-java/src/main/java/sop/Ready.java b/sop-java/src/main/java/sop/Ready.java deleted file mode 100644 index 71ab26e..0000000 --- a/sop-java/src/main/java/sop/Ready.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public abstract class Ready { - - /** - * Write the data to the provided output stream. - * - * @param outputStream output stream - * @throws IOException in case of an IO error - */ - public abstract void writeTo(OutputStream outputStream) throws IOException; - - /** - * Return the data as a byte array by writing it to a {@link ByteArrayOutputStream} first and then returning - * the array. - * - * @return data as byte array - * @throws IOException in case of an IO error - */ - public byte[] getBytes() throws IOException { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - writeTo(bytes); - return bytes.toByteArray(); - } - - /** - * Return an input stream containing the data. - * - * @return input stream - * @throws IOException in case of an IO error - */ - public InputStream getInputStream() throws IOException { - return new ByteArrayInputStream(getBytes()); - } -} diff --git a/sop-java/src/main/java/sop/ReadyWithResult.java b/sop-java/src/main/java/sop/ReadyWithResult.java deleted file mode 100644 index 9feedda..0000000 --- a/sop-java/src/main/java/sop/ReadyWithResult.java +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -import sop.exception.SOPGPException; - -public abstract class ReadyWithResult { - - /** - * Write the data e.g. decrypted plaintext to the provided output stream and return the result of the - * processing operation. - * - * @param outputStream output stream - * @return result, eg. signatures - * - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature if there are no valid signatures found - */ - public abstract T writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature; - - /** - * Return the data as a {@link ByteArrayAndResult}. - * Calling {@link ByteArrayAndResult#getBytes()} will give you access to the data as byte array, while - * {@link ByteArrayAndResult#getResult()} will grant access to the appended result. - * - * @return byte array and result - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature if there are no valid signatures found - */ - public ByteArrayAndResult toByteArrayAndResult() throws IOException, SOPGPException.NoSignature { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - T result = writeTo(bytes); - return new ByteArrayAndResult<>(bytes.toByteArray(), result); - } -} diff --git a/sop-java/src/main/java/sop/SOP.java b/sop-java/src/main/java/sop/SOP.java deleted file mode 100644 index 284a0ab..0000000 --- a/sop-java/src/main/java/sop/SOP.java +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import sop.operation.Armor; -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.Version; - -/** - * Stateless OpenPGP Interface. - */ -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(); - -} diff --git a/sop-java/src/main/java/sop/SessionKey.java b/sop-java/src/main/java/sop/SessionKey.java deleted file mode 100644 index 2adcec4..0000000 --- a/sop-java/src/main/java/sop/SessionKey.java +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.util.Arrays; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import sop.util.HexUtil; - -public class SessionKey { - - private static final Pattern PATTERN = Pattern.compile("^(\\d):([0-9a-fA-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) { - 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 "" + (int) getAlgorithm() + ':' + HexUtil.bytesToHex(sessionKey); - } -} diff --git a/sop-java/src/main/java/sop/Signatures.java b/sop-java/src/main/java/sop/Signatures.java deleted file mode 100644 index dd3f000..0000000 --- a/sop-java/src/main/java/sop/Signatures.java +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.IOException; -import java.io.OutputStream; - -public abstract class Signatures extends Ready { - - /** - * Write OpenPGP signatures to the provided output stream. - * - * @param signatureOutputStream output stream - * @throws IOException in case of an IO error - */ - @Override - public abstract void writeTo(OutputStream signatureOutputStream) throws IOException; - -} diff --git a/sop-java/src/main/java/sop/SigningResult.java b/sop-java/src/main/java/sop/SigningResult.java deleted file mode 100644 index 2cb142d..0000000 --- a/sop-java/src/main/java/sop/SigningResult.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -/** - * This class contains various information about a signed message. - */ -public final class SigningResult { - - private final MicAlg micAlg; - - private SigningResult(MicAlg micAlg) { - this.micAlg = micAlg; - } - - /** - * Return a string identifying the digest mechanism used to create the signed message. - * This is useful for setting the micalg= parameter for the multipart/signed - * content type of a PGP/MIME object as described in section 5 of [RFC3156]. - * - * If more than one signature was generated and different digest mechanisms were used, - * the value of the micalg object is an empty string. - * - * @return micalg - */ - public MicAlg getMicAlg() { - return micAlg; - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - - private MicAlg micAlg; - - public Builder setMicAlg(MicAlg micAlg) { - this.micAlg = micAlg; - return this; - } - - public SigningResult build() { - SigningResult signingResult = new SigningResult(micAlg); - return signingResult; - } - } -} diff --git a/sop-java/src/main/java/sop/Verification.java b/sop-java/src/main/java/sop/Verification.java deleted file mode 100644 index 2047c3d..0000000 --- a/sop-java/src/main/java/sop/Verification.java +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.util.Date; - -import sop.util.UTCUtil; - -public class Verification { - - private final Date creationTime; - private final String signingKeyFingerprint; - private final String signingCertFingerprint; - - public Verification(Date creationTime, String signingKeyFingerprint, String signingCertFingerprint) { - this.creationTime = creationTime; - this.signingKeyFingerprint = signingKeyFingerprint; - this.signingCertFingerprint = signingCertFingerprint; - } - - /** - * Return the signatures' creation time. - * - * @return signature creation time - */ - public Date getCreationTime() { - return creationTime; - } - - /** - * Return the fingerprint of the signing (sub)key. - * - * @return signing key fingerprint - */ - public String getSigningKeyFingerprint() { - return signingKeyFingerprint; - } - - /** - * Return the fingerprint fo the signing certificate. - * - * @return signing certificate fingerprint - */ - public String getSigningCertFingerprint() { - return signingCertFingerprint; - } - - @Override - public String toString() { - return UTCUtil.formatUTCDate(getCreationTime()) + - ' ' + - getSigningKeyFingerprint() + - ' ' + - getSigningCertFingerprint(); - } -} diff --git a/sop-java/src/main/java/sop/enums/ArmorLabel.java b/sop-java/src/main/java/sop/enums/ArmorLabel.java deleted file mode 100644 index aeaa6f9..0000000 --- a/sop-java/src/main/java/sop/enums/ArmorLabel.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum ArmorLabel { - Auto, - Sig, - Key, - Cert, - Message -} diff --git a/sop-java/src/main/java/sop/enums/EncryptAs.java b/sop-java/src/main/java/sop/enums/EncryptAs.java deleted file mode 100644 index 85a2cd7..0000000 --- a/sop-java/src/main/java/sop/enums/EncryptAs.java +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum EncryptAs { - Binary, - Text -} diff --git a/sop-java/src/main/java/sop/enums/InlineSignAs.java b/sop-java/src/main/java/sop/enums/InlineSignAs.java deleted file mode 100644 index b0b55dc..0000000 --- a/sop-java/src/main/java/sop/enums/InlineSignAs.java +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum InlineSignAs { - - /** - * Signature is made over the binary message. - */ - Binary, - - /** - * Signature is made over the message in text mode. - */ - Text, - - /** - * Signature is made using the Cleartext Signature Framework. - */ - CleartextSigned, -} - diff --git a/sop-java/src/main/java/sop/enums/SignAs.java b/sop-java/src/main/java/sop/enums/SignAs.java deleted file mode 100644 index f7f3671..0000000 --- a/sop-java/src/main/java/sop/enums/SignAs.java +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum SignAs { - /** - * Signature is made over the binary message. - */ - Binary, - - /** - * Signature is made over the message in text mode. - */ - Text -} diff --git a/sop-java/src/main/java/sop/enums/package-info.java b/sop-java/src/main/java/sop/enums/package-info.java deleted file mode 100644 index 67148d3..0000000 --- a/sop-java/src/main/java/sop/enums/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - * Enumerations. - */ -package sop.enums; diff --git a/sop-java/src/main/java/sop/exception/SOPGPException.java b/sop-java/src/main/java/sop/exception/SOPGPException.java deleted file mode 100644 index eca2476..0000000 --- a/sop-java/src/main/java/sop/exception/SOPGPException.java +++ /dev/null @@ -1,345 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.exception; - -public abstract class SOPGPException extends RuntimeException { - - public SOPGPException() { - super(); - } - - public SOPGPException(String message) { - super(message); - } - - public SOPGPException(Throwable e) { - super(e); - } - - public SOPGPException(String message, Throwable cause) { - super(message, cause); - } - - public abstract int getExitCode(); - - /** - * No acceptable signatures found (sop verify, inline-verify). - */ - public static class NoSignature extends SOPGPException { - - public static final int EXIT_CODE = 3; - - public NoSignature() { - super("No verifiable signature found."); - } - - 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); - } - - @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(String s) { - super(s); - } - - @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; - - @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(); - } - - @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; - - @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; - } - } -} diff --git a/sop-java/src/main/java/sop/exception/package-info.java b/sop-java/src/main/java/sop/exception/package-info.java deleted file mode 100644 index 4abc562..0000000 --- a/sop-java/src/main/java/sop/exception/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - * Exception classes. - */ -package sop.exception; diff --git a/sop-java/src/main/java/sop/operation/AbstractSign.java b/sop-java/src/main/java/sop/operation/AbstractSign.java deleted file mode 100644 index 3f9d6fc..0000000 --- a/sop-java/src/main/java/sop/operation/AbstractSign.java +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; - -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(Charset.forName("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 deleted file mode 100644 index 51d84b7..0000000 --- a/sop-java/src/main/java/sop/operation/AbstractVerify.java +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Date; - -/** - * Common API methods shared between verification of inline signatures ({@link InlineVerify}) - * and verification of detached signatures ({@link DetachedVerify}). - * - * @param Builder type ({@link DetachedVerify}, {@link InlineVerify}) - */ -public interface AbstractVerify { - - /** - * Makes the SOP implementation consider signatures before this date invalid. - * - * @param timestamp timestamp - * @return builder instance - */ - T notBefore(Date timestamp) - throws SOPGPException.UnsupportedOption; - - /** - * Makes the SOP implementation consider signatures after this date invalid. - * - * @param timestamp timestamp - * @return builder instance - */ - T notAfter(Date timestamp) - throws SOPGPException.UnsupportedOption; - - /** - * Add one or more verification cert. - * - * @param cert input stream containing the encoded certs - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the input stream does not contain an OpenPGP certificate - * @throws IOException in case of an IO error - */ - T cert(InputStream cert) - throws SOPGPException.BadData, - IOException; - - /** - * Add one or more verification cert. - * - * @param cert byte array containing the encoded certs - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP certificate - * @throws IOException in case of an IO error - */ - default T cert(byte[] cert) - throws SOPGPException.BadData, - IOException { - return cert(new ByteArrayInputStream(cert)); - } - -} diff --git a/sop-java/src/main/java/sop/operation/Armor.java b/sop-java/src/main/java/sop/operation/Armor.java deleted file mode 100644 index a625808..0000000 --- a/sop-java/src/main/java/sop/operation/Armor.java +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; - -public interface Armor { - - /** - * Overrides automatic detection of label. - * - * @param label armor label - * @return builder instance - */ - Armor label(ArmorLabel label) - throws SOPGPException.UnsupportedOption; - - /** - * Armor the provided data. - * - * @param data input stream of unarmored OpenPGP data - * @return armored data - * - * @throws sop.exception.SOPGPException.BadData if the data appears to be OpenPGP packets, but those are broken - * @throws IOException in case of an IO error - */ - Ready data(InputStream data) - throws SOPGPException.BadData, - IOException; - - /** - * Armor the provided data. - * - * @param data unarmored OpenPGP data - * @return armored data - * - * @throws sop.exception.SOPGPException.BadData if the data appears to be OpenPGP packets, but those are broken - * @throws IOException in case of an IO error - */ - default Ready data(byte[] data) - throws SOPGPException.BadData, - IOException { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/java/sop/operation/Dearmor.java b/sop-java/src/main/java/sop/operation/Dearmor.java deleted file mode 100644 index 380c4fc..0000000 --- a/sop-java/src/main/java/sop/operation/Dearmor.java +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.exception.SOPGPException; - -public interface Dearmor { - - /** - * Dearmor armored OpenPGP data. - * - * @param data armored OpenPGP data - * @return input stream of unarmored data - * - * @throws sop.exception.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 sop.exception.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)); - } -} diff --git a/sop-java/src/main/java/sop/operation/Decrypt.java b/sop-java/src/main/java/sop/operation/Decrypt.java deleted file mode 100644 index d695032..0000000 --- a/sop-java/src/main/java/sop/operation/Decrypt.java +++ /dev/null @@ -1,193 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -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(Charset.forName("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 deleted file mode 100644 index 745077d..0000000 --- a/sop-java/src/main/java/sop/operation/DetachedSign.java +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.ReadyWithResult; -import sop.SigningResult; -import sop.enums.SignAs; -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -public interface DetachedSign extends AbstractSign { - - /** - * Sets the signature mode. - * Note: This method has to be called before {@link #key(InputStream)} is called. - * - * @param mode signature mode - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - DetachedSign mode(SignAs mode) - throws SOPGPException.UnsupportedOption; - - /** - * Signs data. - * - * @param data input stream containing data - * @return ready - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - * @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered - */ - ReadyWithResult data(InputStream data) - throws IOException, - SOPGPException.KeyIsProtected, - SOPGPException.ExpectedText; - - /** - * Signs data. - * - * @param data byte array containing data - * @return ready - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - * @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered - */ - default ReadyWithResult data(byte[] data) - throws IOException, - SOPGPException.KeyIsProtected, - SOPGPException.ExpectedText { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/java/sop/operation/DetachedVerify.java b/sop-java/src/main/java/sop/operation/DetachedVerify.java deleted file mode 100644 index 9dee870..0000000 --- a/sop-java/src/main/java/sop/operation/DetachedVerify.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * API for verifying detached signatures. - */ -public interface DetachedVerify extends AbstractVerify, VerifySignatures { - - /** - * Provides the detached signatures. - * @param signatures input stream containing encoded, detached signatures. - * - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the input stream does not contain OpenPGP signatures - * @throws IOException in case of an IO error - */ - VerifySignatures signatures(InputStream signatures) - throws SOPGPException.BadData, - IOException; - - /** - * Provides the detached signatures. - * @param signatures byte array containing encoded, detached signatures. - * - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain OpenPGP signatures - * @throws IOException in case of an IO error - */ - default VerifySignatures signatures(byte[] signatures) - throws SOPGPException.BadData, - IOException { - return signatures(new ByteArrayInputStream(signatures)); - } -} diff --git a/sop-java/src/main/java/sop/operation/Encrypt.java b/sop-java/src/main/java/sop/operation/Encrypt.java deleted file mode 100644 index 09e5f12..0000000 --- a/sop-java/src/main/java/sop/operation/Encrypt.java +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; - -import sop.Ready; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; - -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(Charset.forName("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)); - } - - /** - * 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 deleted file mode 100644 index a862d33..0000000 --- a/sop-java/src/main/java/sop/operation/ExtractCert.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.exception.SOPGPException; - -public interface ExtractCert { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - ExtractCert noArmor(); - - /** - * Extract the cert(s) from the provided key(s). - * - * @param keyInputStream input stream containing the encoding of one or more OpenPGP keys - * @return result containing the encoding of the keys certs - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not contain an OpenPGP key - */ - Ready key(InputStream keyInputStream) - throws IOException, - SOPGPException.BadData; - - /** - * Extract the cert(s) from the provided key(s). - * - * @param key byte array containing the encoding of one or more OpenPGP key - * @return result containing the encoding of the keys certs - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP key - */ - default Ready key(byte[] key) - throws IOException, - SOPGPException.BadData { - return key(new ByteArrayInputStream(key)); - } -} diff --git a/sop-java/src/main/java/sop/operation/GenerateKey.java b/sop-java/src/main/java/sop/operation/GenerateKey.java deleted file mode 100644 index af7275b..0000000 --- a/sop-java/src/main/java/sop/operation/GenerateKey.java +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.IOException; -import java.io.InputStream; - -import 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 { - return withKeyPassword(UTF8Util.decodeUTF8(password)); - } - - /** - * 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 deleted file mode 100644 index aba40b1..0000000 --- a/sop-java/src/main/java/sop/operation/InlineDetach.java +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.ReadyWithResult; -import sop.Signatures; -import sop.exception.SOPGPException; - -/** - * Split cleartext signed messages up into data and signatures. - */ -public interface InlineDetach { - - /** - * Do not wrap the signatures in ASCII armor. - * @return builder - */ - InlineDetach noArmor(); - - /** - * Detach the provided signed message from its signatures. - * - * @param messageInputStream input stream containing the signed message - * @return result containing the detached message - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.BadData if the input stream does not contain a signed message - */ - ReadyWithResult message(InputStream messageInputStream) - throws IOException, - SOPGPException.BadData; - - /** - * Detach the provided cleartext signed message from its signatures. - * - * @param message byte array containing the signed message - * @return result containing the detached message - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain a signed message - */ - default ReadyWithResult message(byte[] message) - throws IOException, - SOPGPException.BadData { - return message(new ByteArrayInputStream(message)); - } -} diff --git a/sop-java/src/main/java/sop/operation/InlineSign.java b/sop-java/src/main/java/sop/operation/InlineSign.java deleted file mode 100644 index d45aebd..0000000 --- a/sop-java/src/main/java/sop/operation/InlineSign.java +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.Ready; -import sop.enums.InlineSignAs; -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -public interface InlineSign extends AbstractSign { - - /** - * Sets the signature mode. - * Note: This method has to be called before {@link #key(InputStream)} is called. - * - * @param mode signature mode - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - InlineSign mode(InlineSignAs mode) - throws SOPGPException.UnsupportedOption; - - /** - * Signs data. - * - * @param data input stream containing data - * @return ready - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - * @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered - */ - Ready data(InputStream data) - throws IOException, - SOPGPException.KeyIsProtected, - SOPGPException.ExpectedText; - - /** - * Signs data. - * - * @param data byte array containing data - * @return ready - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - * @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered - */ - default Ready data(byte[] data) - throws IOException, - SOPGPException.KeyIsProtected, - SOPGPException.ExpectedText { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/java/sop/operation/InlineVerify.java b/sop-java/src/main/java/sop/operation/InlineVerify.java deleted file mode 100644 index ac662a0..0000000 --- a/sop-java/src/main/java/sop/operation/InlineVerify.java +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.ReadyWithResult; -import sop.Verification; -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -/** - * API for verification of inline-signed messages. - */ -public interface InlineVerify extends AbstractVerify { - - /** - * Provide the inline-signed data. - * The result can be used to write the plaintext message out and to get the verifications. - * - * @param data signed data - * @return list of signature verifications - * - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - ReadyWithResult> data(InputStream data) - throws IOException, - SOPGPException.NoSignature, - SOPGPException.BadData; - - /** - * Provide the inline-signed data. - * The result can be used to write the plaintext message out and to get the verifications. - * - * @param data signed data - * @return list of signature verifications - * - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - default ReadyWithResult> data(byte[] data) - throws IOException, - SOPGPException.NoSignature, - SOPGPException.BadData { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/java/sop/operation/VerifySignatures.java b/sop-java/src/main/java/sop/operation/VerifySignatures.java deleted file mode 100644 index 5181514..0000000 --- a/sop-java/src/main/java/sop/operation/VerifySignatures.java +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -import sop.Verification; -import sop.exception.SOPGPException; - -public interface VerifySignatures { - - /** - * Provide the signed data (without signatures). - * - * @param data signed data - * @return list of signature verifications - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no valid signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - List data(InputStream data) - throws IOException, - SOPGPException.NoSignature, - SOPGPException.BadData; - - /** - * Provide the signed data (without signatures). - * - * @param data signed data - * @return list of signature verifications - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no valid signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - default List data(byte[] data) - throws IOException, - SOPGPException.NoSignature, - SOPGPException.BadData { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/java/sop/operation/Version.java b/sop-java/src/main/java/sop/operation/Version.java deleted file mode 100644 index 0b50993..0000000 --- a/sop-java/src/main/java/sop/operation/Version.java +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -public interface Version { - - /** - * Return the implementations name. - * e.g. "SOP", - * - * @return implementation name - */ - String getName(); - - /** - * Return the implementations short version string. - * e.g. "1.0" - * - * @return version string - */ - String getVersion(); - - /** - * Return version information about the used OpenPGP backend. - * e.g. "Bouncycastle 1.70" - * - * @return backend version string - */ - String getBackendVersion(); - - /** - * Return an extended version string containing multiple lines of version information. - * The first line MUST match the information produced by {@link #getName()} and {@link #getVersion()}, but the rest of the text - * has no defined structure. - * Example: - *
-     *     "SOP 1.0
-     *     Awesome PGP!
-     *     Using Bouncycastle 1.70
-     *     LibFoo 1.2.2
-     *     See https://pgp.example.org/sop/ for more information"
-     * 
- * - * @return extended version string - */ - String getExtendedVersion(); -} diff --git a/sop-java/src/main/java/sop/operation/package-info.java b/sop-java/src/main/java/sop/operation/package-info.java deleted file mode 100644 index dde4d5b..0000000 --- a/sop-java/src/main/java/sop/operation/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - * Different cryptographic operations. - */ -package sop.operation; diff --git a/sop-java/src/main/java/sop/package-info.java b/sop-java/src/main/java/sop/package-info.java deleted file mode 100644 index 5ad4f52..0000000 --- a/sop-java/src/main/java/sop/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - */ -package sop; diff --git a/sop-java/src/main/java/sop/util/HexUtil.java b/sop-java/src/main/java/sop/util/HexUtil.java deleted file mode 100644 index 9b88f53..0000000 --- a/sop-java/src/main/java/sop/util/HexUtil.java +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2021 Paul Schaub, @maybeWeCouldStealAVan, @Dave L. -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -public class HexUtil { - - private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); - - /** - * Encode a byte array to a hex string. - * - * @see - * How to convert a byte array to a hex string in Java? - * @param bytes bytes - * @return hex encoding - */ - public static String bytesToHex(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = HEX_ARRAY[v >>> 4]; - hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; - } - return new String(hexChars); - } - - /** - * Decode a hex string into a byte array. - * - * @see - * Convert a string representation of a hex dump to a byte array using Java? - * @param s hex string - * @return decoded byte array - */ - public static byte[] hexToBytes(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i + 1), 16)); - } - return data; - } -} diff --git a/sop-java/src/main/java/sop/util/Optional.java b/sop-java/src/main/java/sop/util/Optional.java deleted file mode 100644 index 00eb201..0000000 --- a/sop-java/src/main/java/sop/util/Optional.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -/** - * Backport of java.util.Optional for older Android versions. - * - * @param item type - */ -public class Optional { - - private final T item; - - public Optional() { - this(null); - } - - public Optional(T item) { - this.item = item; - } - - public static Optional of(T item) { - if (item == null) { - throw new NullPointerException("Item cannot be null."); - } - return new Optional<>(item); - } - - public static Optional ofNullable(T item) { - return new Optional<>(item); - } - - public static Optional ofEmpty() { - return new Optional<>(null); - } - - public T get() { - return item; - } - - public boolean isPresent() { - return item != null; - } - - public boolean isEmpty() { - return item == null; - } -} diff --git a/sop-java/src/main/java/sop/util/ProxyOutputStream.java b/sop-java/src/main/java/sop/util/ProxyOutputStream.java deleted file mode 100644 index 0559e8f..0000000 --- a/sop-java/src/main/java/sop/util/ProxyOutputStream.java +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -/** - * {@link OutputStream} that buffers data being written into it, until its underlying output stream is being replaced. - * At that point, first all the buffered data is being written to the underlying stream, followed by any successive - * data that may get written to the {@link ProxyOutputStream}. - * - * This class is useful if we need to provide an {@link OutputStream} at one point in time when the final - * target output stream is not yet known. - */ -public class ProxyOutputStream extends OutputStream { - - private final ByteArrayOutputStream buffer; - private OutputStream swapped; - - public ProxyOutputStream() { - this.buffer = new ByteArrayOutputStream(); - } - - public synchronized void replaceOutputStream(OutputStream underlying) throws IOException { - if (underlying == null) { - throw new NullPointerException("Underlying OutputStream cannot be null."); - } - this.swapped = underlying; - - byte[] bufferBytes = buffer.toByteArray(); - swapped.write(bufferBytes); - } - - @Override - public synchronized void write(byte[] b) throws IOException { - if (swapped == null) { - buffer.write(b); - } else { - swapped.write(b); - } - } - - @Override - public synchronized void write(byte[] b, int off, int len) throws IOException { - if (swapped == null) { - buffer.write(b, off, len); - } else { - swapped.write(b, off, len); - } - } - - @Override - public synchronized void flush() throws IOException { - buffer.flush(); - if (swapped != null) { - swapped.flush(); - } - } - - @Override - public synchronized void close() throws IOException { - buffer.close(); - if (swapped != null) { - swapped.close(); - } - } - - @Override - public synchronized void write(int i) throws IOException { - if (swapped == null) { - buffer.write(i); - } else { - swapped.write(i); - } - } -} diff --git a/sop-java/src/main/java/sop/util/UTCUtil.java b/sop-java/src/main/java/sop/util/UTCUtil.java deleted file mode 100644 index 8ef7e77..0000000 --- a/sop-java/src/main/java/sop/util/UTCUtil.java +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -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 - */ - public static Date parseUTCDate(String dateString) { - for (SimpleDateFormat parser : UTC_PARSERS) { - try { - return parser.parse(dateString); - } catch (ParseException e) { - // Try next parser - } - } - return null; - } - - /** - * 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 deleted file mode 100644 index ec3c22d..0000000 --- a/sop-java/src/main/java/sop/util/UTF8Util.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import sop.exception.SOPGPException; - -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 { - - private static final CharsetDecoder UTF8Decoder = Charset.forName("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 - */ - public static String decodeUTF8(byte[] data) { - ByteBuffer byteBuffer = ByteBuffer.wrap(data); - try { - CharBuffer charBuffer = UTF8Decoder.decode(byteBuffer); - return charBuffer.toString(); - } catch (CharacterCodingException e) { - throw new SOPGPException.PasswordNotHumanReadable(); - } - } -} diff --git a/sop-java/src/main/java/sop/util/package-info.java b/sop-java/src/main/java/sop/util/package-info.java deleted file mode 100644 index 3dd9fc1..0000000 --- a/sop-java/src/main/java/sop/util/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Utility classes. - */ -package sop.util; diff --git a/sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt b/sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt new file mode 100644 index 0000000..021a2d9 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.io.InputStream + +/** + * Tuple of a [ByteArray] and associated result object. + * + * @param bytes byte array + * @param result result object + * @param type of result + */ +data class ByteArrayAndResult(val bytes: ByteArray, val result: T) { + + /** + * [InputStream] returning the contents of [bytes]. + * + * @return input stream + */ + val inputStream: InputStream + get() = bytes.inputStream() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ByteArrayAndResult<*> + + if (!bytes.contentEquals(other.bytes)) return false + if (result != other.result) return false + + return true + } + + override fun hashCode(): Int { + var hashCode = bytes.contentHashCode() + hashCode = 31 * hashCode + (result?.hashCode() ?: 0) + return hashCode + } +} diff --git a/sop-java/src/main/kotlin/sop/DecryptionResult.kt b/sop-java/src/main/kotlin/sop/DecryptionResult.kt new file mode 100644 index 0000000..b653297 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/DecryptionResult.kt @@ -0,0 +1,15 @@ +// 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 new file mode 100644 index 0000000..1284f89 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/EncryptionResult.kt @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import sop.util.Optional + +class EncryptionResult(sessionKey: SessionKey?) { + val sessionKey: Optional + + init { + this.sessionKey = Optional.ofNullable(sessionKey) + } +} diff --git a/sop-java/src/main/kotlin/sop/MicAlg.kt b/sop-java/src/main/kotlin/sop/MicAlg.kt new file mode 100644 index 0000000..58ce7b5 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/MicAlg.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.io.OutputStream +import java.io.PrintWriter + +data class MicAlg(val micAlg: String) { + + fun writeTo(outputStream: OutputStream) { + PrintWriter(outputStream).use { it.write(micAlg) } + } + + companion object { + @JvmStatic fun empty() = MicAlg("") + + @JvmStatic + fun fromHashAlgorithmId(id: Int) = + when (id) { + 1 -> "pgp-md5" + 2 -> "pgp-sha1" + 3 -> "pgp-ripemd160" + 8 -> "pgp-sha256" + 9 -> "pgp-sha384" + 10 -> "pgp-sha512" + 11 -> "pgp-sha224" + 12 -> "pgp-sha3-256" + 14 -> "pgp-sha3-512" + else -> throw IllegalArgumentException("Unsupported hash algorithm ID: $id") + }.let { MicAlg(it) } + } +} diff --git a/sop-java/src/main/kotlin/sop/Profile.kt b/sop-java/src/main/kotlin/sop/Profile.kt new file mode 100644 index 0000000..fd58c63 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/Profile.kt @@ -0,0 +1,124 @@ +// 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 new file mode 100644 index 0000000..1eb3fb3 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/Ready.kt @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +/** Abstract class that encapsulates output data, waiting to be consumed. */ +abstract class Ready { + + /** + * Write the data to the provided output stream. + * + * @param outputStream output stream + * @throws IOException in case of an IO error + */ + @Throws(IOException::class) abstract fun writeTo(outputStream: OutputStream) + + /** + * Return the data as a byte array by writing it to a [ByteArrayOutputStream] first and then + * returning the array. + * + * @return data as byte array + * @throws IOException in case of an IO error + */ + val bytes: ByteArray + @Throws(IOException::class) + get() = + ByteArrayOutputStream() + .let { + writeTo(it) + it + } + .toByteArray() + + /** + * Return an input stream containing the data. + * + * @return input stream + * @throws IOException in case of an IO error + */ + val inputStream: InputStream + @Throws(IOException::class) get() = ByteArrayInputStream(bytes) +} diff --git a/sop-java/src/main/kotlin/sop/ReadyWithResult.kt b/sop-java/src/main/kotlin/sop/ReadyWithResult.kt new file mode 100644 index 0000000..d309c76 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/ReadyWithResult.kt @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.OutputStream +import sop.exception.SOPGPException + +abstract class ReadyWithResult { + + /** + * Write the data e.g. decrypted plaintext to the provided output stream and return the result + * of the processing operation. + * + * @param outputStream output stream + * @return result, eg. signatures + * @throws IOException in case of an IO error + * @throws SOPGPException in case of a SOP protocol error + */ + @Throws(IOException::class, SOPGPException::class) + abstract fun writeTo(outputStream: OutputStream): T + + /** + * Return the data as a [ByteArrayAndResult]. Calling [ByteArrayAndResult.bytes] will give you + * access to the data as byte array, while [ByteArrayAndResult.result] will grant access to the + * appended result. + * + * @return byte array and result + * @throws IOException in case of an IO error + * @throws SOPGPException.NoSignature if there are no valid signatures found + */ + @Throws(IOException::class, SOPGPException::class) + fun toByteArrayAndResult() = + ByteArrayOutputStream().let { + val result = writeTo(it) + ByteArrayAndResult(it.toByteArray(), result) + } +} diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt new file mode 100644 index 0000000..a942c56 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -0,0 +1,71 @@ +// 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 new file mode 100644 index 0000000..d483194 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/SOPV.kt @@ -0,0 +1,52 @@ +// 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 new file mode 100644 index 0000000..af230f3 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/SessionKey.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import sop.util.HexUtil + +/** + * Class representing a symmetric session key. + * + * @param algorithm symmetric key algorithm ID + * @param key [ByteArray] containing the session key + */ +data class SessionKey(val algorithm: Byte, val key: ByteArray) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SessionKey + + if (algorithm != other.algorithm) return false + if (!key.contentEquals(other.key)) return false + + return true + } + + override fun hashCode(): Int { + var hashCode = algorithm.toInt() + hashCode = 31 * hashCode + key.contentHashCode() + return hashCode + } + + override fun toString(): String = "$algorithm:${HexUtil.bytesToHex(key)}" + + companion object { + + @JvmStatic private val PATTERN = "^(\\d):([0-9A-F]+)$".toPattern() + + @JvmStatic + fun fromString(string: String): SessionKey { + val matcher = PATTERN.matcher(string.trim().uppercase().replace("\n", "")) + require(matcher.matches()) { "Provided session key does not match expected format." } + return SessionKey(matcher.group(1).toByte(), HexUtil.hexToBytes(matcher.group(2))) + } + } +} diff --git a/sop-java/src/main/kotlin/sop/Signatures.kt b/sop-java/src/main/kotlin/sop/Signatures.kt new file mode 100644 index 0000000..63aafe9 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/Signatures.kt @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.io.IOException +import java.io.OutputStream + +abstract class Signatures : Ready() { + + /** + * Write OpenPGP signatures to the provided output stream. + * + * @param outputStream signature output stream + * @throws IOException in case of an IO error + */ + @Throws(IOException::class) abstract override fun writeTo(outputStream: OutputStream) +} diff --git a/sop-java/src/main/kotlin/sop/SigningResult.kt b/sop-java/src/main/kotlin/sop/SigningResult.kt new file mode 100644 index 0000000..651f8c1 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/SigningResult.kt @@ -0,0 +1,30 @@ +// 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 new file mode 100644 index 0000000..982e691 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/Verification.kt @@ -0,0 +1,171 @@ +// 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/EncryptAs.kt b/sop-java/src/main/kotlin/sop/enums/EncryptAs.kt new file mode 100644 index 0000000..0b6aa8e --- /dev/null +++ b/sop-java/src/main/kotlin/sop/enums/EncryptAs.kt @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.enums + +enum class EncryptAs { + binary, + text +} diff --git a/sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt b/sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt new file mode 100644 index 0000000..3440edf --- /dev/null +++ b/sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.enums + +enum class InlineSignAs { + + /** Signature is made over the binary message. */ + binary, + + /** Signature is made over the message in text mode. */ + text, + + /** Signature is made using the Cleartext Signature Framework. */ + clearsigned +} diff --git a/sop-java/src/main/kotlin/sop/enums/SignAs.kt b/sop-java/src/main/kotlin/sop/enums/SignAs.kt new file mode 100644 index 0000000..832e831 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/enums/SignAs.kt @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.enums + +enum class SignAs { + /** Signature is made over the binary message. */ + binary, + /** Signature is made over the message in text mode. */ + text +} diff --git a/sop-java/src/main/kotlin/sop/enums/SignatureMode.kt b/sop-java/src/main/kotlin/sop/enums/SignatureMode.kt new file mode 100644 index 0000000..7213195 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/enums/SignatureMode.kt @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.enums + +/** + * Enum referencing relevant signature types. + * + * @see RFC4880 §5.2.1 - Signature + * Types + */ +enum class SignatureMode { + /** Signature of a binary document (type `0x00`). */ + binary, + /** Signature of a canonical text document (type `0x01`). */ + text +} diff --git a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt new file mode 100644 index 0000000..9df1628 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt @@ -0,0 +1,416 @@ +// 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 new file mode 100644 index 0000000..72b8f72 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt @@ -0,0 +1,88 @@ +// 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 new file mode 100644 index 0000000..3554ea3 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/AbstractVerify.kt @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import java.util.* +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.UnsupportedOption + +/** + * Common API methods shared between verification of inline signatures ([InlineVerify]) and + * verification of detached signatures ([DetachedVerify]). + * + * @param Builder type ([DetachedVerify], [InlineVerify]) + */ +interface AbstractVerify { + + /** + * Makes the SOP implementation consider signatures before this date invalid. + * + * @param timestamp timestamp + * @return builder instance + */ + @Throws(UnsupportedOption::class) fun notBefore(timestamp: Date): T + + /** + * Makes the SOP implementation consider signatures after this date invalid. + * + * @param timestamp timestamp + * @return builder instance + */ + @Throws(UnsupportedOption::class) fun notAfter(timestamp: Date): T + + /** + * Add one or more verification cert. + * + * @param cert input stream containing the encoded certs + * @return builder instance + * @throws BadData if the input stream does not contain an OpenPGP certificate + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) fun cert(cert: InputStream): T + + /** + * Add one or more verification cert. + * + * @param cert byte array containing the encoded certs + * @return builder instance + * @throws BadData if the byte array does not contain an OpenPGP certificate + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) + fun cert(cert: ByteArray): T = cert(cert.inputStream()) +} diff --git a/sop-java/src/main/kotlin/sop/operation/Armor.kt b/sop-java/src/main/kotlin/sop/operation/Armor.kt new file mode 100644 index 0000000..b54aed7 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/Armor.kt @@ -0,0 +1,35 @@ +// 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 new file mode 100644 index 0000000..d59f9f0 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt @@ -0,0 +1,117 @@ +// 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 new file mode 100644 index 0000000..fe9b8c9 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt @@ -0,0 +1,120 @@ +// 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 new file mode 100644 index 0000000..2984f27 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/Dearmor.kt @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.Ready +import sop.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 new file mode 100644 index 0000000..4d009f9 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/Decrypt.kt @@ -0,0 +1,166 @@ +// 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 new file mode 100644 index 0000000..4aaadc1 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt @@ -0,0 +1,51 @@ +// 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 new file mode 100644 index 0000000..319658d --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt @@ -0,0 +1,35 @@ +// 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 new file mode 100644 index 0000000..02c7f97 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import 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 new file mode 100644 index 0000000..6485bc2 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.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 new file mode 100644 index 0000000..bccd372 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt @@ -0,0 +1,92 @@ +// 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 new file mode 100644 index 0000000..1cc64ce --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt @@ -0,0 +1,44 @@ +// 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 new file mode 100644 index 0000000..6855a61 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/InlineSign.kt @@ -0,0 +1,48 @@ +// 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 new file mode 100644 index 0000000..a944957 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.ReadyWithResult +import sop.Verification +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.NoSignature + +/** 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 new file mode 100644 index 0000000..0bed1f8 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import sop.Profile + +/** 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 new file mode 100644 index 0000000..20469cb --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt @@ -0,0 +1,64 @@ +// 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 new file mode 100644 index 0000000..13c6712 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt @@ -0,0 +1,78 @@ +// 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 new file mode 100644 index 0000000..13a4bdc --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt @@ -0,0 +1,124 @@ +// 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 new file mode 100644 index 0000000..971de25 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt @@ -0,0 +1,90 @@ +// 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 new file mode 100644 index 0000000..00a64aa --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt @@ -0,0 +1,39 @@ +// 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 new file mode 100644 index 0000000..8a4c808 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/Version.kt @@ -0,0 +1,127 @@ +// 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 new file mode 100644 index 0000000..f372137 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/util/HexUtil.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util + +class HexUtil { + + companion object { + /** + * Encode a byte array to a hex string. + * + * @param bytes bytes + * @return hex encoding + * @see + * [Convert Byte Arrays to Hex Strings in Kotlin](https://www.baeldung.com/kotlin/byte-arrays-to-hex-strings) + */ + @JvmStatic fun bytesToHex(bytes: ByteArray): String = bytes.toHex() + + /** + * Decode a hex string into a byte array. + * + * @param s hex string + * @return decoded byte array + * @see + * [Kotlin convert hex string to ByteArray](https://stackoverflow.com/a/66614516/11150851) + */ + @JvmStatic fun hexToBytes(s: String): ByteArray = s.decodeHex() + } +} + +fun String.decodeHex(): ByteArray { + check(length % 2 == 0) { "Hex encoding must have even number of digits." } + + return chunked(2).map { it.toInt(16).toByte() }.toByteArray() +} + +fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02X".format(eachByte) } diff --git a/sop-java/src/main/kotlin/sop/util/Optional.kt b/sop-java/src/main/kotlin/sop/util/Optional.kt new file mode 100644 index 0000000..0344d0b --- /dev/null +++ b/sop-java/src/main/kotlin/sop/util/Optional.kt @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util + +/** + * Backport of java.util.Optional for older Android versions. + * + * @param item type + */ +data class Optional(val item: T? = null) { + + val isPresent: Boolean = item != null + val isEmpty: Boolean = item == null + + fun get() = item + + companion object { + @JvmStatic fun of(item: T) = Optional(item!!) + + @JvmStatic fun ofNullable(item: T?) = Optional(item) + + @JvmStatic fun ofEmpty() = Optional(null as T?) + } +} diff --git a/sop-java/src/main/kotlin/sop/util/UTCUtil.kt b/sop-java/src/main/kotlin/sop/util/UTCUtil.kt new file mode 100644 index 0000000..4ae13bc --- /dev/null +++ b/sop-java/src/main/kotlin/sop/util/UTCUtil.kt @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util + +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + +class UTCUtil { + + companion object { + + @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 new file mode 100644 index 0000000..770f32c --- /dev/null +++ b/sop-java/src/main/kotlin/sop/util/UTF8Util.kt @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util + +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.nio.charset.CodingErrorAction + +class UTF8Util { + companion object { + @JvmField val UTF8: Charset = Charset.forName("UTF8") + + @JvmStatic + private val UTF8Decoder = + UTF8.newDecoder() + .onUnmappableCharacter(CodingErrorAction.REPORT) + .onMalformedInput(CodingErrorAction.REPORT) + + /** + * Detect non-valid UTF8 data. + * + * @param data utf-8 encoded bytes + * @return decoded string + * @throws CharacterCodingException if the input data does not resemble UTF8 + * @see [ante on StackOverflow](https://stackoverflow.com/a/1471193) + */ + @JvmStatic + @Throws(CharacterCodingException::class) + fun decodeUTF8(data: ByteArray): String { + val byteBuffer = ByteBuffer.wrap(data) + val charBuffer = UTF8Decoder.decode(byteBuffer) + return charBuffer.toString() + } + } +} diff --git a/sop-java/src/main/resources/sop-java-version.properties b/sop-java/src/main/resources/sop-java-version.properties new file mode 100644 index 0000000..a2f509b --- /dev/null +++ b/sop-java/src/main/resources/sop-java-version.properties @@ -0,0 +1,4 @@ +# 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/util/ByteArrayAndResultTest.java b/sop-java/src/test/java/sop/ByteArrayAndResultTest.java similarity index 88% rename from sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java rename to sop-java/src/test/java/sop/ByteArrayAndResultTest.java index 8ae1859..9f9d9ce 100644 --- a/sop-java/src/test/java/sop/util/ByteArrayAndResultTest.java +++ b/sop-java/src/test/java/sop/ByteArrayAndResultTest.java @@ -2,23 +2,23 @@ // // SPDX-License-Identifier: Apache-2.0 -package sop.util; +package sop; + +import org.junit.jupiter.api.Test; +import sop.util.UTCUtil; + +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.Collections; +import java.util.List; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.Test; -import sop.ByteArrayAndResult; -import sop.Verification; - public class ByteArrayAndResultTest { @Test - public void testCreationAndGetters() { + public void testCreationAndGetters() throws ParseException { byte[] bytes = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); List result = Collections.singletonList( new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"), diff --git a/sop-java/src/test/java/sop/util/MicAlgTest.java b/sop-java/src/test/java/sop/MicAlgTest.java similarity index 93% rename from sop-java/src/test/java/sop/util/MicAlgTest.java rename to sop-java/src/test/java/sop/MicAlgTest.java index f720c85..7c6b30a 100644 --- a/sop-java/src/test/java/sop/util/MicAlgTest.java +++ b/sop-java/src/test/java/sop/MicAlgTest.java @@ -2,24 +2,23 @@ // // SPDX-License-Identifier: Apache-2.0 -package sop.util; +package sop; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.HashMap; -import java.util.Map; - -import org.junit.jupiter.api.Test; -import sop.MicAlg; - public class MicAlgTest { @Test public void constructorNullArgThrows() { - assertThrows(IllegalArgumentException.class, () -> new MicAlg(null)); + assertThrows(NullPointerException.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 new file mode 100644 index 0000000..f418672 --- /dev/null +++ b/sop-java/src/test/java/sop/ProfileTest.java @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +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; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ProfileTest { + + @Test + public void toStringFull() { + Profile profile = new Profile("default", "Use the implementers recommendations."); + assertEquals("default: Use the implementers recommendations.", profile.toString()); + } + + @Test + public void 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"); + assertEquals("default", profile.toString()); + } + + @Test + public void parseFull() { + String string = "default: Use the implementers recommendations."; + Profile profile = Profile.parse(string); + assertEquals("default", profile.getName()); + assertTrue(profile.hasDescription()); + assertEquals("Use the implementers recommendations.", profile.getDescription().get()); + } + + @Test + public void parseNameOnly() { + String string = "rfc4880"; + Profile profile = Profile.parse(string); + assertEquals("rfc4880", profile.getName()); + assertFalse(profile.hasDescription()); + } + + @Test + public void parseEmptyDescription() { + String string = "rfc4880: "; + Profile profile = Profile.parse(string); + assertEquals("rfc4880", profile.getName()); + assertFalse(profile.hasDescription()); + + string = "rfc4880:"; + profile = Profile.parse(string); + assertEquals("rfc4880", profile.getName()); + assertFalse(profile.hasDescription()); + } + + @Test + public void parseTooLongProfile() { + // 1200 chars + String string = "longDescription: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."; + assertThrows(IllegalArgumentException.class, () -> Profile.parse(string)); + } + + @Test + public void constructTooLongProfile() { + // name + description = 1200 chars + String name = "longDescription"; + String description = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."; + assertThrows(IllegalArgumentException.class, () -> new Profile(name, description)); + } + + @Test + public void nameCannotBeEmpty() { + assertThrows(IllegalArgumentException.class, () -> new Profile("")); + assertThrows(IllegalArgumentException.class, () -> new Profile(""), "Description Text."); + } + + @Test + public void nameCannotContainColons() { + assertThrows(IllegalArgumentException.class, () -> new Profile("default:")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default:", "DescriptionText")); + assertThrows(IllegalArgumentException.class, () -> new Profile("rfc:4880")); + assertThrows(IllegalArgumentException.class, () -> new Profile("rfc:4880", "OpenPGP Message Format")); + } + + @Test + public void nameCannotContainWhitespace() { + assertThrows(IllegalArgumentException.class, () -> new Profile("default profile")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default profile", "With description.")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\nprofile")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\nprofile", "With description")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\tprofile")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\tprofile", "With description")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\r\nprofile")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\r\nprofile", "With description")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\rprofile")); + assertThrows(IllegalArgumentException.class, () -> new Profile("default\rprofile", "With description")); + } +} diff --git a/sop-java/src/test/java/sop/util/ReadyTest.java b/sop-java/src/test/java/sop/ReadyTest.java similarity index 95% rename from sop-java/src/test/java/sop/util/ReadyTest.java rename to sop-java/src/test/java/sop/ReadyTest.java index 07fa090..49c297b 100644 --- a/sop-java/src/test/java/sop/util/ReadyTest.java +++ b/sop-java/src/test/java/sop/ReadyTest.java @@ -2,16 +2,15 @@ // // SPDX-License-Identifier: Apache-2.0 -package sop.util; +package sop; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; -import org.junit.jupiter.api.Test; -import sop.Ready; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; public class ReadyTest { diff --git a/sop-java/src/test/java/sop/util/ReadyWithResultTest.java b/sop-java/src/test/java/sop/ReadyWithResultTest.java similarity index 92% rename from sop-java/src/test/java/sop/util/ReadyWithResultTest.java rename to sop-java/src/test/java/sop/ReadyWithResultTest.java index 97841fa..88231dc 100644 --- a/sop-java/src/test/java/sop/util/ReadyWithResultTest.java +++ b/sop-java/src/test/java/sop/ReadyWithResultTest.java @@ -2,27 +2,26 @@ // // SPDX-License-Identifier: Apache-2.0 -package sop.util; +package sop; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; +import sop.exception.SOPGPException; +import sop.util.UTCUtil; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.text.ParseException; import java.util.Collections; import java.util.List; -import org.junit.jupiter.api.Test; -import sop.ByteArrayAndResult; -import sop.ReadyWithResult; -import sop.Verification; -import sop.exception.SOPGPException; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; public class ReadyWithResultTest { @Test - public void testReadyWithResult() throws SOPGPException.NoSignature, IOException { + public void testReadyWithResult() throws SOPGPException.NoSignature, IOException, ParseException { byte[] data = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); List result = Collections.singletonList( new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"), diff --git a/sop-java/src/test/java/sop/util/SessionKeyTest.java b/sop-java/src/test/java/sop/SessionKeyTest.java similarity index 85% rename from sop-java/src/test/java/sop/util/SessionKeyTest.java rename to sop-java/src/test/java/sop/SessionKeyTest.java index 2891d0d..6dd3c90 100644 --- a/sop-java/src/test/java/sop/util/SessionKeyTest.java +++ b/sop-java/src/test/java/sop/SessionKeyTest.java @@ -2,15 +2,15 @@ // // SPDX-License-Identifier: Apache-2.0 -package sop.util; +package sop; + +import org.junit.jupiter.api.Test; +import sop.util.HexUtil; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import org.junit.jupiter.api.Test; -import sop.SessionKey; - public class SessionKeyTest { @Test @@ -20,6 +20,14 @@ public class SessionKeyTest { assertEquals(string, sessionKey.toString()); } + @Test + public void fromLowerStringTest() { + String string = "9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"; + String lowercaseWithTrailingNewLine = "9:fca4beaf687f48059cacc14fb019125cd57392bab7037c707835925cbf9f7bcd\n"; + SessionKey sessionKey = SessionKey.fromString(lowercaseWithTrailingNewLine); + assertEquals(string, sessionKey.toString()); + } + @Test public void toStringTest() { SessionKey sessionKey = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); diff --git a/sop-java/src/test/java/sop/util/SigningResultTest.java b/sop-java/src/test/java/sop/SigningResultTest.java similarity index 88% rename from sop-java/src/test/java/sop/util/SigningResultTest.java rename to sop-java/src/test/java/sop/SigningResultTest.java index 0d35cdc..b0e1afd 100644 --- a/sop-java/src/test/java/sop/util/SigningResultTest.java +++ b/sop-java/src/test/java/sop/SigningResultTest.java @@ -2,13 +2,11 @@ // // SPDX-License-Identifier: Apache-2.0 -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; +package sop; import org.junit.jupiter.api.Test; -import sop.MicAlg; -import sop.SigningResult; + +import static org.junit.jupiter.api.Assertions.assertEquals; public class SigningResultTest { diff --git a/sop-java/src/test/java/sop/VerificationJSONTest.java b/sop-java/src/test/java/sop/VerificationJSONTest.java new file mode 100644 index 0000000..a6e5d96 --- /dev/null +++ b/sop-java/src/test/java/sop/VerificationJSONTest.java @@ -0,0 +1,166 @@ +// 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 new file mode 100644 index 0000000..1e10f61 --- /dev/null +++ b/sop-java/src/test/java/sop/VerificationTest.java @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop; + +import org.junit.jupiter.api.Test; +import sop.enums.SignatureMode; +import sop.testsuite.assertions.VerificationAssert; +import sop.util.UTCUtil; + +import java.text.ParseException; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class VerificationTest { + + @Test + public void limitedConstructorTest() throws ParseException { + Date signDate = UTCUtil.parseUTCDate("2022-11-07T15:01:24Z"); + String keyFP = "F9E6F53F7201C60A87064EAB0B27F2B0760A1209"; + String certFP = "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B"; + 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) + .isCreatedAt(signDate) + .hasMode(null) + .hasDescription(null); + } + + @Test + public void limitedParsingTest() throws ParseException { + String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B"; + Verification verification = Verification.fromString(string); + assertEquals(string, verification.toString()); + VerificationAssert.assertThatVerification(verification) + .isCreatedAt(UTCUtil.parseUTCDate("2022-11-07T15:01:24Z")) + .issuedBy("F9E6F53F7201C60A87064EAB0B27F2B0760A1209", "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B") + .hasMode(null) + .hasDescription(null); + } + + @Test + public void parsingWithModeTest() throws ParseException { + String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B mode:text"; + Verification verification = Verification.fromString(string); + assertEquals(string, verification.toString()); + VerificationAssert.assertThatVerification(verification) + .isCreatedAt(UTCUtil.parseUTCDate("2022-11-07T15:01:24Z")) + .issuedBy("F9E6F53F7201C60A87064EAB0B27F2B0760A1209", "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B") + .hasMode(SignatureMode.text) + .hasDescription(null); + } + + @Test + public void extendedConstructorTest() throws ParseException { + Date signDate = UTCUtil.parseUTCDate("2022-11-07T15:01:24Z"); + String keyFP = "F9E6F53F7201C60A87064EAB0B27F2B0760A1209"; + String certFP = "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B"; + SignatureMode mode = SignatureMode.binary; + String description = "certificate from dkg.asc"; + Verification verification = new Verification(signDate, keyFP, certFP, mode, description); + + assertEquals("2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B mode:binary certificate from dkg.asc", verification.toString()); + VerificationAssert.assertThatVerification(verification) + .isCreatedAt(signDate) + .issuedBy(keyFP, certFP) + .hasMode(SignatureMode.binary) + .hasDescription(description); + } + + @Test + public void extendedParsingTest() throws ParseException { + String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B mode:binary certificate from dkg.asc"; + Verification verification = Verification.fromString(string); + assertEquals(string, verification.toString()); + + // no mode + string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B certificate from dkg.asc"; + verification = Verification.fromString(string); + assertEquals(string, verification.toString()); + VerificationAssert.assertThatVerification(verification) + .isCreatedAt(UTCUtil.parseUTCDate("2022-11-07T15:01:24Z")) + .issuedBy("F9E6F53F7201C60A87064EAB0B27F2B0760A1209", "4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B") + .hasMode(null) + .hasDescription("certificate from dkg.asc"); + } + + @Test + public void missingFingerprintFails() { + String string = "2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209"; + assertThrows(IllegalArgumentException.class, () -> Verification.fromString(string)); + } + + @Test + public void malformedTimestampFails() { + String shorter = "'99-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B"; + assertThrows(IllegalArgumentException.class, () -> Verification.fromString(shorter)); + + String longer = "'99-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B mode:binary certificate from dkg.asc"; + assertThrows(IllegalArgumentException.class, () -> Verification.fromString(longer)); + + } +} diff --git a/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java b/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java deleted file mode 100644 index 9d99fd4..0000000 --- a/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -import org.junit.jupiter.api.Test; - -public class ProxyOutputStreamTest { - - @Test - public void replaceOutputStreamThrowsNPEForNull() { - ProxyOutputStream proxy = new ProxyOutputStream(); - assertThrows(NullPointerException.class, () -> proxy.replaceOutputStream(null)); - } - - @Test - public void testSwappingStreamPreservesWrittenBytes() throws IOException { - byte[] firstSection = "Foo\nBar\n".getBytes(StandardCharsets.UTF_8); - byte[] secondSection = "Baz\n".getBytes(StandardCharsets.UTF_8); - - ProxyOutputStream proxy = new ProxyOutputStream(); - proxy.write(firstSection); - - ByteArrayOutputStream swappedStream = new ByteArrayOutputStream(); - proxy.replaceOutputStream(swappedStream); - - proxy.write(secondSection); - proxy.close(); - - assertEquals("Foo\nBar\nBaz\n", swappedStream.toString()); - } -} diff --git a/sop-java/src/test/java/sop/util/UTCUtilTest.java b/sop-java/src/test/java/sop/util/UTCUtilTest.java index 18de817..f6ccbc8 100644 --- a/sop-java/src/test/java/sop/util/UTCUtilTest.java +++ b/sop-java/src/test/java/sop/util/UTCUtilTest.java @@ -4,12 +4,13 @@ package sop.util; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import org.junit.jupiter.api.Test; +import java.text.ParseException; import java.util.Date; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; /** * Test parsing some date examples from the stateless OpenPGP CLI spec. @@ -19,30 +20,29 @@ import org.junit.jupiter.api.Test; public class UTCUtilTest { @Test - public void parseExample1() { + public void parseExample1() throws ParseException { String timestamp = "2019-10-29T12:11:04+00:00"; Date date = UTCUtil.parseUTCDate(timestamp); assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date)); } @Test - public void parseExample2() { + public void parseExample2() throws ParseException { String timestamp = "2019-10-24T23:48:29Z"; Date date = UTCUtil.parseUTCDate(timestamp); assertEquals("2019-10-24T23:48:29Z", UTCUtil.formatUTCDate(date)); } @Test - public void parseExample3() { + public void parseExample3() throws ParseException { String timestamp = "20191029T121104Z"; Date date = UTCUtil.parseUTCDate(timestamp); assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date)); } @Test - public void invalidDateReturnsNull() { + public void invalidDateThrows() { String invalidTimestamp = "foobar"; - Date expectNull = UTCUtil.parseUTCDate(invalidTimestamp); - assertNull(expectNull); + assertThrows(ParseException.class, () -> UTCUtil.parseUTCDate(invalidTimestamp)); } } diff --git a/sop-java/src/test/java/sop/util/UTF8UtilTest.java b/sop-java/src/test/java/sop/util/UTF8UtilTest.java index 775d273..95906bc 100644 --- a/sop-java/src/test/java/sop/util/UTF8UtilTest.java +++ b/sop-java/src/test/java/sop/util/UTF8UtilTest.java @@ -5,8 +5,8 @@ package sop.util; import org.junit.jupiter.api.Test; -import sop.exception.SOPGPException; +import java.nio.charset.CharacterCodingException; import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -15,7 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; public class UTF8UtilTest { @Test - public void testValidUtf8Decoding() { + public void testValidUtf8Decoding() throws CharacterCodingException { String utf8String = "Hello, World\n"; String decoded = UTF8Util.decodeUTF8(utf8String.getBytes(StandardCharsets.UTF_8)); @@ -29,11 +29,11 @@ public class UTF8UtilTest { */ @Test public void testInvalidUtf8StringThrows() { - assertThrows(SOPGPException.PasswordNotHumanReadable.class, + assertThrows(CharacterCodingException.class, () -> UTF8Util.decodeUTF8(new byte[] {(byte) 0xa0, (byte) 0xa1})); - assertThrows(SOPGPException.PasswordNotHumanReadable.class, + assertThrows(CharacterCodingException.class, () -> UTF8Util.decodeUTF8(new byte[] {(byte) 0xc0, (byte) 0xaf})); - assertThrows(SOPGPException.PasswordNotHumanReadable.class, + assertThrows(CharacterCodingException.class, () -> UTF8Util.decodeUTF8(new byte[] {(byte) 0x80, (byte) 0xbf})); } } diff --git a/version.gradle b/version.gradle index ff0c7f7..bac96da 100644 --- a/version.gradle +++ b/version.gradle @@ -4,14 +4,15 @@ allprojects { ext { - shortVersion = '4.0.0' - isSnapshot = false - minAndroidSdk = 10 - javaSourceCompatibility = 1.8 - junitVersion = '5.8.2' - junitSysExitVersion = '1.1.2' - picocliVersion = '4.6.3' - mockitoVersion = '4.5.1' + shortVersion = '14.0.1' + isSnapshot = true + javaSourceCompatibility = 11 + gsonVersion = '2.10.1' jsrVersion = '3.0.2' + junitVersion = '5.8.2' + logbackVersion = '1.5.13' + mockitoVersion = '4.5.1' + picocliVersion = '4.6.3' + slf4jVersion = '1.7.36' } }