Compare commits

...

113 commits
10.0.2 ... main

Author SHA1 Message Date
b28e234f21
ExternalTestSuite: Add license header 2025-07-25 14:09:57 +02:00
cc08b76b68
Introduce sop-java-json-gson module 2025-07-25 14:09:06 +02:00
e680f3450a
SOPV: Document since when operations are available 2025-07-25 12:21:06 +02:00
d4e8c14b08
Update documentation, add withKeyPassphrase(CharArray) methods 2025-07-24 12:53:27 +02:00
d32d9b54d7
Depend on junit-platform-suite to avoid needing to inherit test suite for external-sop tests 2025-07-01 15:06:50 +02:00
c651adc0b3
Add VerificationAssert methods, disable tests for unsupported operations 2025-07-01 14:38:13 +02:00
b3223372c6
SOP-Java 14.0.1-SNAPSHOT 2025-06-17 13:45:10 +02:00
9762f1f043
SOP-Java 14.0.0 2025-06-17 13:45:10 +02:00
191ec8c07d
Improve CHANGELOG again 2025-06-17 13:45:09 +02:00
07d0aa6941
Update CHANGELOG 2025-06-17 13:45:09 +02:00
12835bfb8e
Add/fix missing localizations for new SOP commands 2025-06-17 13:45:09 +02:00
04d154f63d
Version: Fix getSopJavaVersion() 2025-06-17 13:45:09 +02:00
01be696f75
Bump version to 14.0.0-SNAPSHOT 2025-06-17 13:45:09 +02:00
86718a2690
Bump logback to 1.5.13 2025-06-17 13:45:08 +02:00
e72e5a15c0
Fix: Pass chars to StringBuilder.append() 2025-06-17 13:45:08 +02:00
ac17000ff1
Clean up unused version literal 2025-06-17 13:45:08 +02:00
2a22cea29b
Remove animalsniffer 2025-06-17 13:45:08 +02:00
8a7fd5cb58
Move validate-userid to SOPV 2025-06-17 13:45:08 +02:00
21766a1f39
Delete ProxyOutputStream and test 2025-06-17 13:45:07 +02:00
cdcbae7e5f
Add test for JSON data parsing and serializing using a dummy implementation 2025-06-17 13:45:07 +02:00
ebfde35422
Add support for JSON POJOs 2025-06-17 13:45:07 +02:00
bff4423f93
Verification: Rename description to jsonOrDescription 2025-06-17 13:45:07 +02:00
5a7a8ae901
MergeCertsTest: do not pass unarmored data
This is done to fix external-sop tests, which rely on environment variables,
which do not play nicely with binary data
2025-06-17 13:45:07 +02:00
79aece6f04
SOP, SOPV: Add --debug option 2025-06-17 13:45:07 +02:00
28d06c330d
ExternalSOP: Map UnspecificError 2025-06-17 13:45:06 +02:00
ab13cc1de1
CertifyUserIdExternal: add separator before passing keys 2025-06-17 13:45:06 +02:00
e1d048225b
CertifyValidateUserIdTest: unbound User-IDs do throw exceptions 2025-06-17 13:45:06 +02:00
61206dde53
GenerateKeyTest: Provoke exception for CertCannotEncrypt test case 2025-06-17 13:45:06 +02:00
589884672a
External-SOP: Properly map KeyCannotCertify error code 2025-06-17 13:45:06 +02:00
47a6db8702
External-SOP: Fix error message typo 2025-06-17 13:45:05 +02:00
0df80470c6
External-SOP: Extend test suite with new test classes 2025-06-17 13:45:05 +02:00
00a02686c8
External-SOP: Fix command names 2025-06-17 13:45:05 +02:00
c5d9e57f69
Add test for encrypt-decrypt using all available generate-key profiles 2025-06-17 13:45:05 +02:00
e481717421
Add Profile.withAliases() utility function 2025-06-17 13:45:05 +02:00
9677f1fd0b
Fix profile constructors 2025-06-17 13:45:05 +02:00
e5cb58468b
Add aliases to Profile 2025-06-17 13:45:04 +02:00
4ef5444e78
Test key generation with supported profiles 2025-06-17 13:45:04 +02:00
77106942d1
Add tests for MergeCerts command 2025-06-17 13:45:04 +02:00
6d23d3771d
Remove unused import 2025-06-17 13:45:04 +02:00
9360b0e8ce
Remove println statements 2025-06-17 13:45:04 +02:00
38c5a947dd
Add MergeCertsTest 2025-06-17 13:45:03 +02:00
be460fabab
Add test for certifying without ASCII armor 2025-06-17 13:45:03 +02:00
138e275bb6
Fix formatting issues 2025-06-17 13:45:03 +02:00
091b5f9a5e
Add test for certifying with revoked key 2025-06-17 13:45:03 +02:00
dea7e905a9
Document update key 2025-06-17 13:45:03 +02:00
8c077a9c13
SOP update-key: Rename --no-new-mechanisms option to --no-added-capabilities 2025-06-17 13:45:03 +02:00
a8cfb8fbf4
Improve test 2025-06-17 13:45:02 +02:00
a8497617d5
Add basic test for certify-userid and validate-userid subcommands 2025-06-17 13:45:02 +02:00
68bab9cbb4
reuse: convert dep5 file to toml file 2025-06-17 13:45:02 +02:00
65aa0afd4e
Add new Exception types 2025-06-17 13:45:02 +02:00
a72545e3b9
Fix formatting 2025-06-17 13:45:02 +02:00
082cbde869
MergeCertsCmd: Fix default value of armor 2025-06-17 13:45:01 +02:00
b300be42a4
validate-userid: Add --validate-at option 2025-06-17 13:45:01 +02:00
5105b6f4ad
Remove call to explicitly set bundle to fix native image 2025-06-17 13:45:01 +02:00
7f1c1b1aae
Fix documentation of merge-certs command 2025-06-17 13:45:01 +02:00
4cf410a9f9
Bump version 2025-06-17 13:45:01 +02:00
f1bdce99cb
Document endOfOptionsDelimiter 2025-06-17 13:45:00 +02:00
f7cc9ab816
Fix nullability of sop commands 2025-06-17 13:45:00 +02:00
40ccb8cc99
Add first test for new commands 2025-06-17 13:45:00 +02:00
0bb50952c5
Show endOfOptions delimiter in help 2025-06-17 13:45:00 +02:00
69fbfc09a7
Implement external variants of new subcommands 2025-06-17 13:45:00 +02:00
dd12e28926
Checkstyle 2025-06-17 13:45:00 +02:00
4ed326a142
Implement validate-userid command 2025-06-17 13:44:59 +02:00
122cd016a1
Update msg files with input/output information 2025-06-17 13:44:59 +02:00
2b6a5dd651
Checkstyle and exception handling improvements 2025-06-17 13:44:59 +02:00
d6c1330874
Implement certify-userid command 2025-06-17 13:44:59 +02:00
ada77be955
Add support for rendering help info for input and output 2025-06-17 13:44:59 +02:00
84404d629f
Add implementation of merge-certs command 2025-06-17 13:44:58 +02:00
4115a5041d
Add implementation of update-key command 2025-06-17 13:44:58 +02:00
1dcf13244d
Add new exceptions 2025-06-17 13:44:58 +02:00
ad137d6351
Try to fix coveralls repo token 2025-05-15 02:16:52 +02:00
cbeec9c90d
SOP-Java 10.1.2-SNAPSHOT 2025-04-14 11:44:22 +02:00
701f9453ca
SOP-Java 10.1.1 2025-04-14 11:41:50 +02:00
2d99aea4ab
Bump animalsniffer to 2.0.0 2025-04-14 11:29:04 +02:00
4d2876a296
Fix formatting issue 2025-04-14 11:28:14 +02:00
e3fe9410d7
reuse: Migrate to toml format 2025-04-14 11:27:54 +02:00
a2a3bda2b3
Migrate AbortOnUnsupportedOption annotation back to java 2025-04-14 11:26:09 +02:00
cddc92bd92
Update changelog 2025-04-14 11:13:40 +02:00
8394f2e5a8
Make use of toolchain functionality and raise min Java API level to 11 2025-04-14 11:01:45 +02:00
2c26ab2da5
Improve reproducibility 2025-04-14 11:01:20 +02:00
859bb5bdde
Fix issues in kdoc 2025-04-04 12:16:00 +02:00
edb405d79e
Add TODO to remove ProxyOutputStream in 11.X 2025-04-04 12:11:20 +02:00
57e2f8391b
Update CHANGELOG 2025-04-04 10:43:49 +02:00
51ba24ddbe
Enable kapt annotation processing to properly embed picocli configuration files for native images into the -cli jar file
For this it is apparently necessary to upgrade kotlin to 1.9.21
See https://stackoverflow.com/a/79030947/11150851
2025-04-03 14:18:46 +02:00
d1893c5ea0
SOP-Java 10.1.1-SNAPSHOT 2025-03-11 21:22:31 +01:00
c145f8bb37
SOP-Java 10.1.0 2025-03-11 21:19:41 +01:00
924cfaa140
Update README 2025-03-11 21:18:24 +01:00
f2602bb413
Bump version to 10.1.0-SNAPSHOT 2025-03-11 21:04:50 +01:00
97e91f50ab
Migrate pipeline definition to use from_secret
https://woodpecker-ci.org/docs/usage/secrets\#use-secrets-in-settings-and-environment
2025-01-28 12:34:53 +01:00
690ba6dc16
Add rpgpie-sop reference in README 2025-01-28 12:34:53 +01:00
9ec3cc911b
Add bcsop reference in README 2025-01-28 12:34:53 +01:00
f92a73a5ad
Add back legacy --verify-out option alias for decrypt cmd 2025-01-28 12:34:53 +01:00
2b6015f59a
Add license header to properties files 2025-01-28 12:34:52 +01:00
84e381fe8e
Write sop-java version to sop-java-version.properties 2025-01-28 12:34:52 +01:00
b1e1a2283f
Update changelog 2025-01-28 12:34:51 +01:00
b3b8da4e35
Move testfixtures to own artifact 2025-01-28 12:33:07 +01:00
ca65cbe668
For now, do not re-set msg bundle (graal) 2025-01-28 12:33:06 +01:00
4eb6d1fdcb
Prevent unmatched parameters when setting locale 2025-01-28 12:33:06 +01:00
594b9029b2
Document logback spam 2025-01-28 12:33:06 +01:00
471947ef9c
Fix woodpecker warnings 2025-01-28 12:33:06 +01:00
1fd3161851
Properly match MissingArg exception code 2025-01-28 12:33:06 +01:00
a8a753536a
Add translations for new hardware exception error messages 2025-01-28 12:33:06 +01:00
eadea08d3c
Add new SOPGPException types related to hardware modules 2025-01-28 12:33:05 +01:00
547acdb740
Remove label() option from armor() operation 2025-01-28 12:33:05 +01:00
bb026bcbeb
Mark ProxyOutputStream as deprecated 2025-01-28 12:33:05 +01:00
e7778cb0d2
Remove deprecated junit5-system-exit
Replaced with custom test DSL that avoids System.exit
2025-01-28 12:33:05 +01:00
ac00b68694
Add description of external-sop module 2025-01-28 12:33:04 +01:00
e6c9d6f43d
SOP-Java 10.0.4-SNAPSHOT 2024-10-31 14:06:37 +01:00
c136d40fa7
SOP-Java 10.0.3 2024-10-31 13:54:31 +01:00
f35fd6c1ae
Update changelog 2024-10-31 13:53:57 +01:00
375dd65789
revoke-key command: Allow for multiple '--with-key-password' options 2024-10-31 13:48:55 +01:00
42a16a4f6d
Fix password parameter passing in change-key-password 2024-10-31 13:48:32 +01:00
b3f446fe8d
SOP-Java 10.0.3-SNAPSHOT 2024-10-14 16:22:06 +02:00
164 changed files with 3648 additions and 927 deletions

View file

@ -1,29 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: SOP-Java
Upstream-Contact: Paul Schaub <info@pgpainless.org>
Source: https://pgpainless.org
# Sample paragraph, commented out:
#
# Files: src/*
# Copyright: $YEAR $NAME <$CONTACT>
# License: ...
# Gradle build tool
Files: gradle*
Copyright: 2015 the original author or authors.
License: Apache-2.0
# Woodpecker build files
Files: .woodpecker/*
Copyright: 2022 the original author or authors.
License: Apache-2.0
Files: external-sop/src/main/resources/sop/testsuite/external/*
Copyright: 2023 the original author or authors
License: Apache-2.0
# Github Issue Templates
Files: .github/ISSUE_TEMPLATE/*
Copyright: 2024 the original author or authors
License: Apache-2.0

View file

@ -1,5 +1,7 @@
steps: steps:
run: run:
when:
event: push
image: gradle:7.6-jdk11-jammy image: gradle:7.6-jdk11-jammy
commands: commands:
# Install Sequoia-SOP # Install Sequoia-SOP
@ -14,4 +16,6 @@ steps:
- gradle check javadocAll - gradle check javadocAll
# Code has coverage # Code has coverage
- gradle jacocoRootReport coveralls - gradle jacocoRootReport coveralls
secrets: [coveralls_repo_token] environment:
COVERALLS_REPO_TOKEN:
from_secret: coveralls_repo_token

View file

@ -2,6 +2,8 @@
# See https://reuse.software/ # See https://reuse.software/
steps: steps:
reuse: reuse:
when:
event: push
image: fsfe/reuse:latest image: fsfe/reuse:latest
commands: commands:
- reuse lint - reuse lint

View file

@ -6,6 +6,47 @@ SPDX-License-Identifier: Apache-2.0
# Changelog # 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 ## 10.0.2
- Downgrade `logback-core` to `1.2.13` - Downgrade `logback-core` to `1.2.13`
@ -25,6 +66,10 @@ SPDX-License-Identifier: Apache-2.0
- Introduce `sopv` interface subset with revision `1.0` - Introduce `sopv` interface subset with revision `1.0`
- Add `sop version --sopv` - 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 ## 8.0.1
- `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty. - `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty.
@ -43,6 +88,13 @@ SPDX-License-Identifier: Apache-2.0
- Change `EncryptAs` values into lowercase - Change `EncryptAs` values into lowercase
- Change `SignAs` 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 ## 7.0.0
- Update implementation to [SOP Specification revision 07](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-07.html). - 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 `revoke-key` subcommand

View file

@ -7,7 +7,7 @@ SPDX-License-Identifier: Apache-2.0
# SOP for Java # SOP for Java
[![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java) [![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java)
[![Spec Revision: 10](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/10/) [![Spec Revision: 14](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/14/)
[![Coverage Status](https://coveralls.io/repos/github/pgpainless/sop-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/sop-java?branch=main) [![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) [![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/sop-java)](https://api.reuse.software/info/github.com/pgpainless/sop-java)
@ -25,6 +25,10 @@ The repository contains the following modules:
* [sop-java](/sop-java) defines a set of Java interfaces describing the Stateless OpenPGP Protocol. * [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 * [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. 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 ## Known Implementations
(Please expand!) (Please expand!)
@ -33,9 +37,11 @@ compatible with the SOP-CLI specification.
|-------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------| |-------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| [pgpainless-sop](https://github.com/pgpainless/pgpainless/tree/main/pgpainless-sop) | Implementation of `sop-java` using PGPainless | | [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` | | [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 ### Implementations in other languages
| Project | Language | | Project | Language |
|-------------------------------------------------|----------| |---------------------------------------------------|----------|
| [sop-rs](https://sequoia-pgp.gitlab.io/sop-rs/) | Rust | | [sop-rs](https://sequoia-pgp.gitlab.io/sop-rs/) | Rust |
| [SOP for python](https://pypi.org/project/sop/) | Python | | [SOP for python](https://pypi.org/project/sop/) | Python |
| [rpgpie-sop](https://crates.io/crates/rpgpie-sop) | Rust |

32
REUSE.toml Normal file
View file

@ -0,0 +1,32 @@
# SPDX-FileCopyrightText: 2025 Paul Schaub <info@pgpainless.org>
#
# SPDX-License-Identifier: CC0-1.0
version = 1
SPDX-PackageName = "SOP-Java"
SPDX-PackageSupplier = "Paul Schaub <info@pgpainless.org>"
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"

View file

@ -18,8 +18,7 @@ buildscript {
} }
plugins { plugins {
id 'ru.vyarus.animalsniffer' version '1.5.3' id 'org.jetbrains.kotlin.jvm' version "1.9.21"
id 'org.jetbrains.kotlin.jvm' version "1.8.10"
id 'com.diffplug.spotless' version '6.22.0' apply false id 'com.diffplug.spotless' version '6.22.0' apply false
} }
@ -32,20 +31,9 @@ allprojects {
apply plugin: 'jacoco' apply plugin: 'jacoco'
apply plugin: 'checkstyle' apply plugin: 'checkstyle'
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.diffplug.spotless' apply plugin: 'com.diffplug.spotless'
// For non-cli modules enable android api compatibility check
if (it.name.equals('sop-java')) {
// animalsniffer
apply plugin: 'ru.vyarus.animalsniffer'
dependencies {
signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:2.3.3_r2@signature"
}
animalsniffer {
sourceSets = [sourceSets.main]
}
}
// Only generate jar for submodules // Only generate jar for submodules
// https://stackoverflow.com/a/25445035 // https://stackoverflow.com/a/25445035
jar { jar {
@ -67,8 +55,6 @@ allprojects {
description = "Stateless OpenPGP Protocol API for Java" description = "Stateless OpenPGP Protocol API for Java"
version = shortVersion version = shortVersion
sourceCompatibility = javaSourceCompatibility
repositories { repositories {
mavenCentral() mavenCentral()
} }
@ -77,6 +63,13 @@ allprojects {
tasks.withType(AbstractArchiveTask) { tasks.withType(AbstractArchiveTask) {
preserveFileTimestamps = false preserveFileTimestamps = false
reproducibleFileOrder = true reproducibleFileOrder = true
dirMode = 0755
fileMode = 0644
}
kotlin {
jvmToolchain(javaSourceCompatibility)
} }
// Compatibility of default implementations in kotlin interfaces with Java implementations. // Compatibility of default implementations in kotlin interfaces with Java implementations.
@ -111,7 +104,7 @@ allprojects {
} }
jacoco { jacoco {
toolVersion = "0.8.7" toolVersion = "0.8.8"
} }
jacocoTestReport { jacocoTestReport {
@ -119,7 +112,7 @@ allprojects {
sourceDirectories.setFrom(project.files(sourceSets.main.allSource.srcDirs)) sourceDirectories.setFrom(project.files(sourceSets.main.allSource.srcDirs))
classDirectories.setFrom(project.files(sourceSets.main.output)) classDirectories.setFrom(project.files(sourceSets.main.output))
reports { reports {
xml.enabled true xml.required = true
} }
} }
@ -137,15 +130,15 @@ subprojects {
apply plugin: 'signing' apply plugin: 'signing'
task sourcesJar(type: Jar, dependsOn: classes) { task sourcesJar(type: Jar, dependsOn: classes) {
classifier = 'sources' archiveClassifier = 'sources'
from sourceSets.main.allSource from sourceSets.main.allSource
} }
task javadocJar(type: Jar, dependsOn: javadoc) { task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc' archiveClassifier = 'javadoc'
from javadoc.destinationDir from javadoc.destinationDir
} }
task testsJar(type: Jar, dependsOn: testClasses) { task testsJar(type: Jar, dependsOn: testClasses) {
classifier = 'tests' archiveClassifier = 'tests'
from sourceSets.test.output from sourceSets.test.output
} }
@ -242,7 +235,7 @@ task jacocoRootReport(type: JacocoReport) {
classDirectories.setFrom(files(subprojects.sourceSets.main.output)) classDirectories.setFrom(files(subprojects.sourceSets.main.output))
executionData.setFrom(files(subprojects.jacocoTestReport.executionData)) executionData.setFrom(files(subprojects.jacocoTestReport.executionData))
reports { reports {
xml.enabled true xml.required = true
xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml") xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml")
} }
// We could remove the following setOnlyIf line, but then // We could remove the following setOnlyIf line, but then
@ -253,10 +246,6 @@ task jacocoRootReport(type: JacocoReport) {
} }
task javadocAll(type: Javadoc) { task javadocAll(type: Javadoc) {
def currentJavaVersion = JavaVersion.current()
if (currentJavaVersion.compareTo(JavaVersion.VERSION_1_9) >= 0) {
options.addStringOption("-release", "8");
}
source subprojects.collect {project -> source subprojects.collect {project ->
project.sourceSets.main.allJava } project.sourceSets.main.allJava }
destinationDir = new File(buildDir, 'javadoc') destinationDir = new File(buildDir, 'javadoc')

View file

@ -15,7 +15,9 @@ repositories {
dependencies { dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
testImplementation "org.junit.platform:junit-platform-suite-api:1.13.2"
testRuntimeOnly 'org.junit.platform:junit-platform-suite:1.13.2'
api project(":sop-java") api project(":sop-java")
api "org.slf4j:slf4j-api:$slf4jVersion" api "org.slf4j:slf4j-api:$slf4jVersion"
@ -27,7 +29,7 @@ dependencies {
// The ExternalTestSubjectFactory reads json config file to find configured SOP binaries... // The ExternalTestSubjectFactory reads json config file to find configured SOP binaries...
testImplementation "com.google.code.gson:gson:$gsonVersion" testImplementation "com.google.code.gson:gson:$gsonVersion"
// ...and extends TestSubjectFactory // ...and extends TestSubjectFactory
testImplementation(testFixtures(project(":sop-java"))) testImplementation(project(":sop-java-testfixtures"))
} }
test { test {

View file

@ -69,6 +69,14 @@ class ExternalSOP(
override fun changeKeyPassword(): ChangeKeyPassword = override fun changeKeyPassword(): ChangeKeyPassword =
ChangeKeyPasswordExternal(binaryName, properties) 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 * 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 * temporarily store additional results of OpenPGP operations such that the binding classes can
@ -112,6 +120,9 @@ class ExternalSOP(
val errorMessage = readString(errIn) val errorMessage = readString(errIn)
when (exitCode) { when (exitCode) {
UnspecificFailure.EXIT_CODE ->
throw UnspecificFailure(
"External SOP backend reported an unspecific error ($exitCode):\n$errorMessage")
NoSignature.EXIT_CODE -> NoSignature.EXIT_CODE ->
throw NoSignature( throw NoSignature(
"External SOP backend reported error NoSignature ($exitCode):\n$errorMessage") "External SOP backend reported error NoSignature ($exitCode):\n$errorMessage")
@ -169,6 +180,21 @@ class ExternalSOP(
UnsupportedProfile.EXIT_CODE -> UnsupportedProfile.EXIT_CODE ->
throw UnsupportedProfile( throw UnsupportedProfile(
"External SOP backend reported error UnsupportedProfile ($exitCode):\n$errorMessage") "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? // Did you forget to add a case for a new exception type?
else -> else ->

View file

@ -10,9 +10,11 @@ import sop.SOPV
import sop.external.ExternalSOP.TempDirProvider import sop.external.ExternalSOP.TempDirProvider
import sop.external.operation.DetachedVerifyExternal import sop.external.operation.DetachedVerifyExternal
import sop.external.operation.InlineVerifyExternal import sop.external.operation.InlineVerifyExternal
import sop.external.operation.ValidateUserIdExternal
import sop.external.operation.VersionExternal import sop.external.operation.VersionExternal
import sop.operation.DetachedVerify import sop.operation.DetachedVerify
import sop.operation.InlineVerify import sop.operation.InlineVerify
import sop.operation.ValidateUserId
import sop.operation.Version import sop.operation.Version
/** /**
@ -37,6 +39,8 @@ class ExternalSOPV(
override fun inlineVerify(): InlineVerify = override fun inlineVerify(): InlineVerify =
InlineVerifyExternal(binaryName, properties, tempDirProvider) InlineVerifyExternal(binaryName, properties, tempDirProvider)
override fun validateUserId(): ValidateUserId = ValidateUserIdExternal(binaryName, properties)
companion object { companion object {
/** /**

View file

@ -7,7 +7,6 @@ package sop.external.operation
import java.io.InputStream import java.io.InputStream
import java.util.Properties import java.util.Properties
import sop.Ready import sop.Ready
import sop.enums.ArmorLabel
import sop.exception.SOPGPException import sop.exception.SOPGPException
import sop.external.ExternalSOP import sop.external.ExternalSOP
import sop.operation.Armor import sop.operation.Armor
@ -18,8 +17,6 @@ class ArmorExternal(binary: String, environment: Properties) : Armor {
private val commandList: MutableList<String> = mutableListOf(binary, "armor") private val commandList: MutableList<String> = mutableListOf(binary, "armor")
private val envList: List<String> = ExternalSOP.propertiesToEnv(environment) private val envList: List<String> = ExternalSOP.propertiesToEnv(environment)
override fun label(label: ArmorLabel): Armor = apply { commandList.add("--label=$label") }
@Throws(SOPGPException.BadData::class) @Throws(SOPGPException.BadData::class)
override fun data(data: InputStream): Ready = override fun data(data: InputStream): Ready =
ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data)

View file

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<String> = 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)
}

View file

@ -37,7 +37,7 @@ class EncryptExternal(
override fun signWith(key: InputStream): Encrypt = apply { override fun signWith(key: InputStream): Encrypt = apply {
commandList.add("--sign-with=@ENV:SIGN_WITH_$argCounter") commandList.add("--sign-with=@ENV:SIGN_WITH_$argCounter")
envList.add("SIGN_WITH_$argCounter=${ExternalSOP.readString(key)}") envList.add("SIGN_WITH_$argCounter=${readString(key)}")
argCounter += 1 argCounter += 1
} }

View file

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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)
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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)
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<String> = 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)}")
}
}

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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 {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.ArmorDearmorTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalArmorDearmorTest extends ArmorDearmorTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.DecryptWithSessionKeyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalDecryptWithSessionKeyTest extends DecryptWithSessionKeyTest {
}

View file

@ -1,12 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.DetachedSignDetachedVerifyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalDetachedSignDetachedVerifyTest extends DetachedSignDetachedVerifyTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.EncryptDecryptTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalEncryptDecryptTest extends EncryptDecryptTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.ExtractCertTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalExtractCertTest extends ExtractCertTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.GenerateKeyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalGenerateKeyTest extends GenerateKeyTest {
}

View file

@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.InlineSignInlineDetachDetachedVerifyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalInlineSignInlineDetachDetachedVerifyTest
extends InlineSignInlineDetachDetachedVerifyTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.InlineSignInlineVerifyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalInlineSignInlineVerifyTest extends InlineSignInlineVerifyTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.ListProfilesTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalListProfilesTest extends ListProfilesTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.RevokeKeyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalRevokeKeyTest extends RevokeKeyTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.VersionTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalVersionTest extends VersionTest {
}

View file

@ -6,5 +6,7 @@ rootProject.name = 'SOP-Java'
include 'sop-java', include 'sop-java',
'sop-java-picocli', 'sop-java-picocli',
'external-sop' 'sop-java-testfixtures',
'external-sop',
'sop-java-json-gson'

View file

@ -0,0 +1,13 @@
<!--
SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
SPDX-License-Identifier: Apache-2.0
-->
# SOP-Java-JSON-GSON
## JSON Parsing VERIFICATION extension JSON using Gson
Since revision 11, the SOP specification defines VERIFICATIONS extension JSON.
This module implements the `JSONParser` and `JSONSerializer` interfaces using Googles Gson library.

View file

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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"
}

View file

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<Verification.JSON>(){}.type)
} catch (e: JsonSyntaxException) {
throw ParseException(e.message, 0)
}
}
}

View file

@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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)
}
}

View file

@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<ParseException> { parser.parse("Invalid") }
}
@Test
fun parseMalformedJSON() {
// Missing '}'
assertThrows<ParseException> { parser.parse("{\"signers\":[\"Alice\"]") }
}
}

View file

@ -12,19 +12,16 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$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 // Mocking Components
testImplementation "org.mockito:mockito-core:$mockitoVersion" testImplementation "org.mockito:mockito-core:$mockitoVersion"
// SOP // SOP
implementation(project(":sop-java")) implementation(project(":sop-java"))
testImplementation(project(":sop-java-testfixtures"))
// CLI // CLI
implementation "info.picocli:picocli:$picocliVersion" implementation "info.picocli:picocli:$picocliVersion"
annotationProcessor "info.picocli:picocli-codegen:$picocliVersion" kapt "info.picocli:picocli-codegen:$picocliVersion"
// @Nonnull, @Nullable... // @Nonnull, @Nullable...
implementation "com.google.code.findbugs:jsr305:$jsrVersion" implementation "com.google.code.findbugs:jsr305:$jsrVersion"
@ -36,6 +33,10 @@ application {
mainClass = mainClassName mainClass = mainClassName
} }
compileJava {
options.compilerArgs += ["-Aproject=${project.group}/${project.name}"]
}
jar { jar {
dependsOn(":sop-java:jar") dependsOn(":sop-java:jar")
duplicatesStrategy(DuplicatesStrategy.EXCLUDE) duplicatesStrategy(DuplicatesStrategy.EXCLUDE)

View file

@ -21,6 +21,8 @@ class SOPExceptionExitCodeMapper : IExitCodeExceptionMapper {
// Unmatched subcommand // Unmatched subcommand
SOPGPException.UnsupportedSubcommand.EXIT_CODE SOPGPException.UnsupportedSubcommand.EXIT_CODE
} }
} else if (exception is MissingParameterException) {
SOPGPException.MissingArg.EXIT_CODE
} else if (exception is ParameterException) { } else if (exception is ParameterException) {
// Invalid option (e.g. `--as invalid`) // Invalid option (e.g. `--as invalid`)
SOPGPException.UnsupportedOption.EXIT_CODE SOPGPException.UnsupportedOption.EXIT_CODE

View file

@ -27,6 +27,10 @@ import sop.exception.SOPGPException
ChangeKeyPasswordCmd::class, ChangeKeyPasswordCmd::class,
RevokeKeyCmd::class, RevokeKeyCmd::class,
ExtractCertCmd::class, ExtractCertCmd::class,
UpdateKeyCmd::class,
MergeCertsCmd::class,
CertifyUserIdCmd::class,
ValidateUserIdCmd::class,
// Messaging subcommands // Messaging subcommands
SignCmd::class, SignCmd::class,
VerifyCmd::class, VerifyCmd::class,
@ -60,7 +64,7 @@ class SopCLI {
@JvmField var EXECUTABLE_NAME = "sop" @JvmField var EXECUTABLE_NAME = "sop"
@JvmField @JvmField
@Option(names = ["--stacktrace"], scope = CommandLine.ScopeType.INHERIT) @Option(names = ["--stacktrace", "--debug"], scope = ScopeType.INHERIT)
var stacktrace = false var stacktrace = false
@JvmStatic @JvmStatic
@ -74,17 +78,21 @@ class SopCLI {
@JvmStatic @JvmStatic
fun execute(vararg args: String): Int { fun execute(vararg args: String): Int {
// Set locale // Set locale
CommandLine(InitLocale()).parseArgs(*args) CommandLine(InitLocale()).setUnmatchedArgumentsAllowed(true).parseArgs(*args)
// Re-set bundle with updated locale // Re-set bundle with updated locale
cliMsg = ResourceBundle.getBundle("msg_sop") cliMsg = ResourceBundle.getBundle("msg_sop")
return CommandLine(SopCLI::class.java) return CommandLine(SopCLI::class.java)
.apply { .apply {
// explicitly set help command resource bundle
subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help"))
// Hide generate-completion command // Hide generate-completion command
subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true) 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 // overwrite executable name
commandName = EXECUTABLE_NAME commandName = EXECUTABLE_NAME
// setup exception handling // setup exception handling

View file

@ -45,7 +45,8 @@ class SopVCLI {
@JvmField var EXECUTABLE_NAME = "sopv" @JvmField var EXECUTABLE_NAME = "sopv"
@JvmField @JvmField
@CommandLine.Option(names = ["--stacktrace"], scope = CommandLine.ScopeType.INHERIT) @CommandLine.Option(
names = ["--stacktrace", "--debug"], scope = CommandLine.ScopeType.INHERIT)
var stacktrace = false var stacktrace = false
@JvmStatic @JvmStatic

View file

@ -7,6 +7,11 @@ package sop.cli.picocli.commands
import java.io.* import java.io.*
import java.text.ParseException import java.text.ParseException
import java.util.* 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.cli.picocli.commands.AbstractSopCmd.EnvironmentVariableResolver
import sop.exception.SOPGPException.* import sop.exception.SOPGPException.*
import sop.util.UTCUtil.Companion.parseUTCDate import sop.util.UTCUtil.Companion.parseUTCDate
@ -215,11 +220,106 @@ abstract class AbstractSopCmd(locale: Locale = Locale.getDefault()) : Runnable {
} }
} }
/**
* See
* [Example](https://github.com/remkop/picocli/blob/main/picocli-examples/src/main/java/picocli/examples/customhelp/EnvironmentVariablesSection.java)
*/
class InputOutputHelpSectionRenderer(private val argument: Pair<String?, String?>) :
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<String>, header: String, details: String): List<String> {
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 { companion object {
const val PRFX_ENV = "@ENV:" const val PRFX_ENV = "@ENV:"
const val PRFX_FD = "@FD:" 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 val DAWN_OF_TIME = Date(0)
@JvmField @JvmField

View file

@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<String> = listOf()
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
var withKeyPassword: List<String> = listOf()
@Option(names = ["--no-require-self-sig"]) var noRequireSelfSig = false
@Parameters(paramLabel = "KEYS", arity = "1..*") var keys: List<String> = 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)
}
}
}

View file

@ -33,9 +33,15 @@ class ChangeKeyPasswordCmd : AbstractSopCmd() {
changeKeyPassword.noArmor() changeKeyPassword.noArmor()
} }
oldKeyPasswords.forEach { changeKeyPassword.oldKeyPassphrase(it) } oldKeyPasswords.forEach {
val password = stringFromInputStream(getInput(it))
changeKeyPassword.oldKeyPassphrase(password)
}
newKeyPassword?.let { changeKeyPassword.newKeyPassphrase(it) } newKeyPassword?.let {
val password = stringFromInputStream(getInput(it))
changeKeyPassword.newKeyPassphrase(password)
}
try { try {
changeKeyPassword.keys(System.`in`).writeTo(System.out) changeKeyPassword.keys(System.`in`).writeTo(System.out)

View file

@ -29,7 +29,7 @@ class DecryptCmd : AbstractSopCmd() {
@Option(names = [OPT_WITH_PASSWORD], paramLabel = "PASSWORD") @Option(names = [OPT_WITH_PASSWORD], paramLabel = "PASSWORD")
var withPassword: List<String> = listOf() var withPassword: List<String> = listOf()
@Option(names = [OPT_VERIFICATIONS_OUT], paramLabel = "VERIFICATIONS") @Option(names = [OPT_VERIFICATIONS_OUT, "--verify-out"], paramLabel = "VERIFICATIONS")
var verifyOut: String? = null var verifyOut: String? = null
@Option(names = [OPT_VERIFY_WITH], paramLabel = "CERT") var certs: List<String> = listOf() @Option(names = [OPT_VERIFY_WITH], paramLabel = "CERT") var certs: List<String> = listOf()

View file

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<String> = 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)
}
}
}

View file

@ -19,8 +19,8 @@ class RevokeKeyCmd : AbstractSopCmd() {
@Option(names = ["--no-armor"], negatable = true) var armor = true @Option(names = ["--no-armor"], negatable = true) var armor = true
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD") @Option(names = ["--with-key-password"], paramLabel = "PASSWORD", arity = "0..*")
var withKeyPassword: String? = null var withKeyPassword: List<String> = listOf()
override fun run() { override fun run() {
val revokeKey = throwIfUnsupportedSubcommand(SopCLI.getSop().revokeKey(), "revoke-key") val revokeKey = throwIfUnsupportedSubcommand(SopCLI.getSop().revokeKey(), "revoke-key")
@ -29,9 +29,9 @@ class RevokeKeyCmd : AbstractSopCmd() {
revokeKey.noArmor() revokeKey.noArmor()
} }
withKeyPassword?.let { for (passwordIn in withKeyPassword) {
try { try {
val password = stringFromInputStream(getInput(it)) val password = stringFromInputStream(getInput(passwordIn))
revokeKey.withKeyPassword(password) revokeKey.withKeyPassword(password)
} catch (e: SOPGPException.UnsupportedOption) { } catch (e: SOPGPException.UnsupportedOption) {
val errorMsg = val errorMsg =

View file

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<String> = listOf()
@Option(names = ["--merge-certs"], paramLabel = "CERTS") var mergeCerts: List<String> = 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)
}
}
}

View file

@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<String> = 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)
}
}
}

View file

@ -2,11 +2,14 @@
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
usage.header=Add ASCII Armor to standard input usage.header=Add ASCII Armor to standard input
label=Label to be used in the header and tail of the armoring
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 stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.optionListHeading=%nOptions:%n
usage.optionListHeading = %nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -2,11 +2,12 @@
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
usage.header=Schütze Standard-Eingabe mit ASCII Armor usage.header=Schütze Standard-Eingabe mit ASCII Armor
label=Label für Kopf- und Fußzeile der 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 stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.optionListHeading=%nOptionen:%n
usage.optionListHeading = %nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# 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

View file

@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# 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

View file

@ -12,10 +12,15 @@ old-key-password.0=Old passwords to unlock the keys with.
old-key-password.1=Multiple passwords can be passed in, which are tested sequentially to unlock locked subkeys. old-key-password.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...). 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 stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nDescription:%n usage.descriptionHeading=%nDescription:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -12,10 +12,13 @@ old-key-password.0=Alte Passw
old-key-password.1=Mehrere Passwortkandidaten können übergeben werden, welche der Reihe nach durchprobiert werden, um Unterschlüssel zu entsperren. old-key-password.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...). 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 stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nBeschreibung:%n usage.descriptionHeading=%nBeschreibung:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -3,9 +3,14 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
usage.header=Remove ASCII Armor from standard input 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 stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -3,9 +3,12 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
usage.header=Entferne ASCII Armor von Standard-Eingabe 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 stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -22,10 +22,15 @@ with-key-password.0=Passphrase to unlock the secret key(s).
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
KEY[0..*]=Secret keys to attempt decryption with KEY[0..*]=Secret keys to attempt decryption with
standardInput=CIPHERTEXT
standardInputDescription=Encrypted OpenPGP message
standardOutput=DATA
standardOutputDescription=Decrypted OpenPGP message
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -22,10 +22,13 @@ with-key-password.0=Passwort zum Entsperren der privaten Schl
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
KEY[0..*]=Private Schlüssel zum Entschlüsseln der Nachricht KEY[0..*]=Private Schlüssel zum Entschlüsseln der Nachricht
standardInputDescription=Verschlüsselte OpenPGP Nachricht
standardOutputDescription=Entschlüsselte OpenPGP Nachricht
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -11,10 +11,15 @@ with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, f
micalg-out=Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156). 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 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 stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -11,10 +11,13 @@ with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable,
micalg-out=Gibt den verwendeten Digest-Algorithmus an die angegebene Ausgabe in einer Form aus, die zum Auffüllen des micalg-Parameters für den PGP/MIME Content-Type (RFC3156) verwendet werden kann. 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 KEYS[0..*]=Private Signaturschlüssel
standardInputDescription=Daten die signiert werden sollen
standardOutputDescription=Abgetrennte OpenPGP Signatur(en)
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -13,11 +13,16 @@ not-after.3=Accepts special value "-" for end of time.
SIGNATURE[0]=Detached signature SIGNATURE[0]=Detached signature
CERT[1..*]=Public key certificates for signature verification 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 stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nDescription:%n usage.descriptionHeading=%nDescription:%n
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -13,11 +13,14 @@ not-after.3=Akzeptiert speziellen Wert '-' f
SIGNATURE[0]=Abgetrennte Signatur SIGNATURE[0]=Abgetrennte Signatur
CERT[1..*]=Zertifikate (öffentliche Schlüssel) zur Signaturprüfung 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 stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nBeschreibung:%n usage.descriptionHeading=%nBeschreibung:%n
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -12,10 +12,15 @@ with-key-password.0=Passphrase to unlock the secret key(s).
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
CERTS[0..*]=Certificates the message gets encrypted to 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 stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -12,10 +12,13 @@ with-key-password.0=Passwort zum Entsperren der privaten Schl
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
CERTS[0..*]=Zertifikate für die die Nachricht verschlüsselt werden soll 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 stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -5,10 +5,15 @@ usage.header=Extract a public key certificate from a secret key
usage.description=Read a secret key from STDIN and emit the public key certificate to STDOUT. usage.description=Read a secret key from STDIN and emit the public key certificate to STDOUT.
no-armor=ASCII armor the output 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 stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nDescription:%n usage.descriptionHeading=%nDescription:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -5,10 +5,13 @@ usage.header=Extrahiere Zertifikat (
usage.description=Lese einen Schlüssel von Standard-Eingabe und gebe das Zertifikat auf Standard-Ausgabe aus. 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 no-armor=Schütze Ausgabe mit ASCII Armor
standardInputDescription=Private Schlüssel, deren Zertifikate extrahiert werden sollen
standardOutputDescription=Extrahierte Zertifikate
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nBeschreibung:%n usage.descriptionHeading=%nBeschreibung:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -9,10 +9,13 @@ signing-only=Generate a key that can only be used for signing
with-key-password.0=Password to protect the private key with with-key-password.0=Password to protect the private key with
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). 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 stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -9,10 +9,12 @@ signing-only=Generiere einen Schl
with-key-password.0=Passwort zum Schutz des privaten Schlüssels with-key-password.0=Passwort zum Schutz des privaten Schlüssels
with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
standardOutputDescription=Erzeugter OpenPGP Schlüssel
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -6,6 +6,6 @@ usage.header=Display usage information for the specified subcommand
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -7,5 +7,5 @@ stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -5,9 +5,14 @@ usage.header=Split signatures from a clearsigned message
no-armor=ASCII armor the output no-armor=ASCII armor the output
signatures-out=Destination to which a detached signatures block will be written 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 stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -5,9 +5,12 @@ usage.header=Trenne Signaturen von Klartext-signierter Nachricht
no-armor=Schütze Ausgabe mit ASCII Armor no-armor=Schütze Ausgabe mit ASCII Armor
signatures-out=Schreibe abgetrennte Signaturen in Ausgabe signatures-out=Schreibe abgetrennte Signaturen in Ausgabe
standardInputDescription=Klartext-signierte OpenPGP Nachricht
standardOutputDescription=Nachricht ohne Signaturen
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -13,10 +13,15 @@ with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, f
micalg=Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156). 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 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 stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -13,10 +13,13 @@ with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable,
micalg=Gibt den verwendeten Digest-Algorithmus an die angegebene Ausgabe in einer Form aus, die zum Auffüllen des micalg-Parameters für den PGP/MIME Content-Type (RFC3156) verwendet werden kann. 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 KEYS[0..*]=Private Signaturschlüssel
standardInputDescription=Daten, die signiert werden sollen
standardOutputDescription=Inline-signierte OpenPGP Nachricht
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -12,10 +12,15 @@ not-after.3=Accepts special value "-" for end of time.
verifications-out=File to write details over successful verifications to verifications-out=File to write details over successful verifications to
CERT[0..*]=Public key certificates for signature verification 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 stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -12,10 +12,13 @@ not-after.3=Akzeptiert speziellen Wert '-' f
verifications-out=Schreibe Status der Signaturprüfung in angegebene Ausgabe verifications-out=Schreibe Status der Signaturprüfung in angegebene Ausgabe
CERT[0..*]=Zertifikate (öffentlich Schlüssel) zur Signaturprüfung CERT[0..*]=Zertifikate (öffentlich Schlüssel) zur Signaturprüfung
standardInputDescription=Inline-signierte OpenPGP Nachricht
standardOutputDescription=Nachricht ohne Signaturen
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -4,10 +4,13 @@
usage.header=Emit a list of profiles supported by the identified subcommand usage.header=Emit a list of profiles supported by the identified subcommand
subcommand=Subcommand for which to list profiles subcommand=Subcommand for which to list profiles
standardOutput=PROFILELIST
standardOutputDescription=List of profiles supported by the identified subcommand
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -4,10 +4,12 @@
usage.header=Gebe eine Liste von Profilen aus, welche vom angegebenen Unterbefehl unterstützt werden usage.header=Gebe eine Liste von Profilen aus, welche vom angegebenen Unterbefehl unterstützt werden
subcommand=Unterbefehl, für welchen Profile gelistet werden sollen subcommand=Unterbefehl, für welchen Profile gelistet werden sollen
standardOutputDescription=Liste von Profilen, die der identifizierte Unterbefehl unterstützt
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# 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

View file

@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# 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

View file

@ -7,10 +7,15 @@ no-armor=ASCII armor the output
with-key-password.0=Passphrase to unlock the secret key(s). 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...). 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 stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nDescription:%n usage.descriptionHeading=D%nescription:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -7,10 +7,13 @@ no-armor=Sch
with-key-password.0=Passwort zum Entsperren der privaten Schlüssel with-key-password.0=Passwort zum Entsperren der privaten Schlüssel
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). 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 stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nBeschreibung:%n usage.descriptionHeading=%nBeschreibung:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -9,10 +9,14 @@ locale=Locale for description texts
# Generic # Generic
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.parameterListHeading=%nParameters:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n
standardInputHeading=%nInput:%n
standardOutputHeading=%nOutput:%n
# Exit Codes # Exit Codes
usage.exitCodeListHeading=%nExit Codes:%n usage.exitCodeListHeading=%nExit Codes:%n
usage.exitCodeList.0=\u00200:Successful program execution usage.exitCodeList.0=\u00200:Successful program execution
@ -36,6 +40,10 @@ usage.exitCodeList.17=73:Ambiguous input (a filename matching the designator alr
usage.exitCodeList.18=79:Key is not signing capable usage.exitCodeList.18=79:Key is not signing capable
usage.exitCodeList.19=83:Options were supplied that are incompatible with each other 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.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 ## SHARED RESOURCES
stacktrace=Print stacktrace stacktrace=Print stacktrace
@ -72,6 +80,8 @@ sop.error.runtime.cert_cannot_encrypt=Certificate from input '%s' cannot encrypt
sop.error.runtime.no_session_key_extracted=Session key not extracted. Feature potentially not supported. sop.error.runtime.no_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.no_verifiable_signature_found=No verifiable signature found.
sop.error.runtime.cannot_decrypt_message=Message could not be decrypted. 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 ## Usage errors
sop.error.usage.password_or_cert_required=At least one password file or cert file required for encryption. 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.argument_required=Argument '%s' is required.

View file

@ -10,9 +10,13 @@ locale=Gebietsschema f
# Generic # Generic
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.parameterListHeading=%nParameter:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n
standardInputHeading=%nEingabe:%n
standardOutputHeading=%nAusgabe:%n
# Exit Codes # Exit Codes
usage.exitCodeListHeading=%nExit Codes:%n usage.exitCodeListHeading=%nExit Codes:%n
usage.exitCodeList.0=\u00200:Erfolgreiche Programmausführung usage.exitCodeList.0=\u00200:Erfolgreiche Programmausführung
@ -36,6 +40,10 @@ usage.exitCodeList.17=73:Mehrdeutige Eingabe (ein Dateiname, der dem Bezeichner
usage.exitCodeList.18=79:Schlüssel ist nicht fähig zu signieren usage.exitCodeList.18=79:Schlüssel ist nicht fähig zu signieren
usage.exitCodeList.19=83:Miteinander inkompatible Optionen spezifiziert 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.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 ## SHARED RESOURCES
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben

View file

@ -0,0 +1,24 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# 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

View file

@ -0,0 +1,21 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# 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

View file

@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# 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

View file

@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# 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

View file

@ -5,10 +5,13 @@ usage.header=Display version information about the tool
extended=Print an extended version string extended=Print an extended version string
backend=Print information about the cryptographic backend backend=Print information about the cryptographic backend
sop-spec=Print the latest revision of the SOP specification targeted by the implementation 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 stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -5,10 +5,13 @@ usage.header=Zeige Versionsinformationen
extended=Gebe erweiterte Versionsinformationen aus extended=Gebe erweiterte Versionsinformationen aus
backend=Gebe Informationen über das kryptografische Backend aus backend=Gebe Informationen über das kryptografische Backend aus
sop-spec=Gebe die neuste Revision der SOP Spezifikation aus, welche von dieser Implementierung umgesetzt wird sop-spec=Gebe die neuste Revision der SOP Spezifikation aus, welche von dieser Implementierung umgesetzt wird
sopv=Gebe die SOPV API Version aus
standardOutput=Versionsinformationen
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -6,16 +6,18 @@ package sop.cli.picocli;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock; 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.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sop.SOP; import sop.SOP;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.operation.Armor; import sop.operation.Armor;
import sop.operation.CertifyUserId;
import sop.operation.ChangeKeyPassword; import sop.operation.ChangeKeyPassword;
import sop.operation.Dearmor; import sop.operation.Dearmor;
import sop.operation.Decrypt; import sop.operation.Decrypt;
@ -28,31 +30,52 @@ import sop.operation.InlineVerify;
import sop.operation.DetachedSign; import sop.operation.DetachedSign;
import sop.operation.DetachedVerify; import sop.operation.DetachedVerify;
import sop.operation.ListProfiles; import sop.operation.ListProfiles;
import sop.operation.MergeCerts;
import sop.operation.RevokeKey; import sop.operation.RevokeKey;
import sop.operation.UpdateKey;
import sop.operation.ValidateUserId;
import sop.operation.Version; import sop.operation.Version;
public class SOPTest { public class SOPTest {
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedSubcommand.EXIT_CODE)
public void assertExitOnInvalidSubcommand() { public void assertExitOnInvalidSubcommand() {
SOP sop = mock(SOP.class); SOP sop = mock(SOP.class);
SopCLI.setSopInstance(sop); SopCLI.setSopInstance(sop);
SopCLI.main(new String[] {"invalid"}); assertUnsupportedSubcommand(() -> SopCLI.execute("invalid"));
} }
@Test @Test
@ExpectSystemExitWithStatus(1)
public void assertThrowsIfNoSOPBackendSet() { public void assertThrowsIfNoSOPBackendSet() {
SopCLI.setSopInstance(null); SopCLI.setSopInstance(null);
// At this point, no SOP backend is set, so an InvalidStateException triggers exit(1) // At this point, no SOP backend is set, so an InvalidStateException triggers error code 1
SopCLI.main(new String[] {"armor"}); assertGenericError(() -> SopCLI.execute("armor"));
} }
@Test @Test
public void UnsupportedSubcommandsTest() { public void UnsupportedSubcommandsTest() {
SOP nullCommandSOP = new SOP() { 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 @Override
public Version version() { public Version version() {
return null; return null;
@ -141,6 +164,11 @@ public class SOPTest {
commands.add(new String[] {"sign"}); commands.add(new String[] {"sign"});
commands.add(new String[] {"verify", "signature.asc", "cert.asc"}); commands.add(new String[] {"verify", "signature.asc", "cert.asc"});
commands.add(new String[] {"version"}); commands.add(new String[] {"version"});
commands.add(new String[] {"list-profiles", "generate-key"});
commands.add(new String[] {"certify-userid", "--userid", "Alice <alice@pgpainless.org>", "--", "alice.pgp"});
commands.add(new String[] {"validate-userid", "Alice <alice@pgpainless.org>", "bob.pgp", "--", "alice.pgp"});
commands.add(new String[] {"update-key"});
commands.add(new String[] {"merge-certs"});
for (String[] command : commands) { for (String[] command : commands) {
int exit = SopCLI.execute(command); int exit = SopCLI.execute(command);

View file

@ -4,8 +4,6 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import com.ginsberg.junit.exit.FailOnSystemExit;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sop.Ready; import sop.Ready;
@ -24,6 +22,8 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
public class ArmorCmdTest { public class ArmorCmdTest {
@ -42,24 +42,22 @@ public class ArmorCmdTest {
@Test @Test
public void assertDataIsAlwaysCalled() throws SOPGPException.BadData, IOException { public void assertDataIsAlwaysCalled() throws SOPGPException.BadData, IOException {
SopCLI.main(new String[] {"armor"}); assertSuccess(() -> SopCLI.execute("armor"));
verify(armor, times(1)).data((InputStream) any()); verify(armor, times(1)).data((InputStream) any());
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void ifBadDataExit41() throws SOPGPException.BadData, IOException { public void ifBadDataExit41() throws SOPGPException.BadData, IOException {
when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"armor"}); assertBadData(() -> SopCLI.execute("armor"));
} }
@Test @Test
@FailOnSystemExit
public void ifNoErrorsNoExit() { public void ifNoErrorsNoExit() {
when(sop.armor()).thenReturn(armor); when(sop.armor()).thenReturn(armor);
SopCLI.main(new String[] {"armor"}); assertSuccess(() -> SopCLI.execute("armor"));
} }
private static Ready nopReady() { private static Ready nopReady() {

View file

@ -9,12 +9,13 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sop.Ready; import sop.Ready;
@ -48,14 +49,13 @@ public class DearmorCmdTest {
@Test @Test
public void assertDataIsCalled() throws IOException, SOPGPException.BadData { public void assertDataIsCalled() throws IOException, SOPGPException.BadData {
SopCLI.main(new String[] {"dearmor"}); assertSuccess(() -> SopCLI.execute("dearmor"));
verify(dearmor, times(1)).data((InputStream) any()); verify(dearmor, times(1)).data((InputStream) any());
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void assertBadDataCausesExit41() throws IOException, SOPGPException.BadData { public void assertBadDataCausesExit41() throws IOException, SOPGPException.BadData {
when(dearmor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException("invalid armor"))); when(dearmor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException("invalid armor")));
SopCLI.main(new String[] {"dearmor"}); assertBadData(() -> SopCLI.execute("dearmor"));
} }
} }

View file

@ -4,7 +4,6 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatcher;
@ -42,6 +41,18 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; 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 { public class DecryptCmdTest {
@ -74,47 +85,47 @@ public class DecryptCmdTest {
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE)
public void missingArgumentsExceptionCausesExit19() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException { public void missingArgumentsExceptionCausesExit19() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException {
when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.MissingArg("Missing arguments.")); when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.MissingArg("Missing arguments."));
SopCLI.main(new String[] {"decrypt"}); assertMissingArg(() -> SopCLI.execute("decrypt"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void badDataExceptionCausesExit41() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException { public void badDataExceptionCausesExit41() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException {
when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"decrypt"}); assertBadData(() -> SopCLI.execute("decrypt"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.PasswordNotHumanReadable.EXIT_CODE)
public void assertNotHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, public void assertNotHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable,
SOPGPException.UnsupportedOption, IOException { SOPGPException.UnsupportedOption, IOException {
File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable"); File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable");
when(decrypt.withPassword(any())).thenThrow(new SOPGPException.PasswordNotHumanReadable()); 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 @Test
public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
File passwordFile = TestFileUtil.writeTempStringFile("orange"); 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"); verify(decrypt, times(1)).withPassword("orange");
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
File passwordFile = TestFileUtil.writeTempStringFile("swordfish"); File passwordFile = TestFileUtil.writeTempStringFile("swordfish");
when(decrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Decrypting with password not supported.")); 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 @Test
public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption { public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption {
Date now = new Date(); 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)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME);
verify(decrypt, times(1)).verifyNotAfter( verify(decrypt, times(1)).verifyNotAfter(
ArgumentMatchers.argThat(argument -> { ArgumentMatchers.argThat(argument -> {
@ -125,7 +136,8 @@ public class DecryptCmdTest {
@Test @Test
public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption { public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"decrypt", "--verify-not-before", "-", "--verify-not-after", "-"}); assertSuccess(() ->
SopCLI.execute("decrypt", "--verify-not-before", "-", "--verify-not-after", "-"));
verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME); verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME);
verify(decrypt, times(1)).verifyNotAfter(AbstractSopCmd.END_OF_TIME); verify(decrypt, times(1)).verifyNotAfter(AbstractSopCmd.END_OF_TIME);
} }
@ -138,54 +150,57 @@ public class DecryptCmdTest {
return Math.abs(now.getTime() - argument.getTime()) <= 1000; return Math.abs(now.getTime() - argument.getTime()) <= 1000;
}; };
SopCLI.main(new String[] {"decrypt", "--verify-not-before", "now", "--verify-not-after", "now"}); assertSuccess(() ->
SopCLI.execute("decrypt", "--verify-not-before", "now", "--verify-not-after", "now"));
verify(decrypt, times(1)).verifyNotAfter(ArgumentMatchers.argThat(isMaxOneSecOff)); verify(decrypt, times(1)).verifyNotAfter(ArgumentMatchers.argThat(isMaxOneSecOff));
verify(decrypt, times(1)).verifyNotBefore(ArgumentMatchers.argThat(isMaxOneSecOff)); verify(decrypt, times(1)).verifyNotBefore(ArgumentMatchers.argThat(isMaxOneSecOff));
} }
@Test @Test
@ExpectSystemExitWithStatus(1)
public void assertMalformedDateInNotBeforeCausesExit1() { public void assertMalformedDateInNotBeforeCausesExit1() {
// ParserException causes exit(1) // ParserException causes exit(1)
SopCLI.main(new String[] {"decrypt", "--verify-not-before", "invalid"}); assertGenericError(() ->
SopCLI.execute("decrypt", "--verify-not-before", "invalid"));
} }
@Test @Test
@ExpectSystemExitWithStatus(1)
public void assertMalformedDateInNotAfterCausesExit1() { public void assertMalformedDateInNotAfterCausesExit1() {
// ParserException causes exit(1) // ParserException causes exit(1)
SopCLI.main(new String[] {"decrypt", "--verify-not-after", "invalid"}); assertGenericError(() ->
SopCLI.execute("decrypt", "--verify-not-after", "invalid"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void assertUnsupportedNotAfterCausesExit37() throws SOPGPException.UnsupportedOption { public void assertUnsupportedNotAfterCausesExit37() throws SOPGPException.UnsupportedOption {
when(decrypt.verifyNotAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); when(decrypt.verifyNotAfter(any())).thenThrow(
SopCLI.main(new String[] {"decrypt", "--verify-not-after", "now"}); new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported."));
assertUnsupportedOption(() ->
SopCLI.execute("decrypt", "--verify-not-after", "now"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void assertUnsupportedNotBeforeCausesExit37() throws SOPGPException.UnsupportedOption { public void assertUnsupportedNotBeforeCausesExit37() throws SOPGPException.UnsupportedOption {
when(decrypt.verifyNotBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); when(decrypt.verifyNotBefore(any())).thenThrow(
SopCLI.main(new String[] {"decrypt", "--verify-not-before", "now"}); new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported."));
assertUnsupportedOption(() ->
SopCLI.execute("decrypt", "--verify-not-before", "now"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE)
public void assertExistingSessionKeyOutFileCausesExit59() throws IOException { public void assertExistingSessionKeyOutFileCausesExit59() throws IOException {
File tempFile = File.createTempFile("existing-session-key-", ".tmp"); File tempFile = File.createTempFile("existing-session-key-", ".tmp");
tempFile.deleteOnExit(); tempFile.deleteOnExit();
SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); assertOutputExists(() ->
SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void assertWhenSessionKeyCannotBeExtractedExit37() throws IOException { public void assertWhenSessionKeyCannotBeExtractedExit37() throws IOException {
Path tempDir = Files.createTempDirectory("session-key-out-dir"); Path tempDir = Files.createTempDirectory("session-key-out-dir");
File tempFile = new File(tempDir.toFile(), "session-key"); File tempFile = new File(tempDir.toFile(), "session-key");
tempFile.deleteOnExit(); tempFile.deleteOnExit();
SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); assertUnsupportedOption(() ->
SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath()));
} }
@Test @Test
@ -210,8 +225,10 @@ public class DecryptCmdTest {
File verificationsFile = new File(tempDir.toFile(), "verifications"); File verificationsFile = new File(tempDir.toFile(), "verifications");
File keyFile = new File(tempDir.toFile(), "key.asc"); File keyFile = new File(tempDir.toFile(), "key.asc");
keyFile.createNewFile(); keyFile.createNewFile();
SopCLI.main(new String[] {"decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(), assertSuccess(() ->
"--verifications-out", verificationsFile.getAbsolutePath(), "--verify-with", keyFile.getAbsolutePath()}); SopCLI.execute("decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(),
"--verifications-out", verificationsFile.getAbsolutePath(), "--verify-with",
keyFile.getAbsolutePath()));
ByteArrayOutputStream bytesInFile = new ByteArrayOutputStream(); ByteArrayOutputStream bytesInFile = new ByteArrayOutputStream();
try (FileInputStream fileIn = new FileInputStream(sessionKeyFile)) { try (FileInputStream fileIn = new FileInputStream(sessionKeyFile)) {
@ -241,10 +258,10 @@ public class DecryptCmdTest {
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.CannotDecrypt.EXIT_CODE)
public void assertUnableToDecryptExceptionResultsInExit29() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { public void assertUnableToDecryptExceptionResultsInExit29() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException {
when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.CannotDecrypt()); when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.CannotDecrypt());
SopCLI.main(new String[] {"decrypt"}); assertCannotDecrypt(() ->
SopCLI.execute("decrypt"));
} }
@Test @Test
@ -258,30 +275,32 @@ public class DecryptCmdTest {
return new DecryptionResult(null, Collections.emptyList()); return new DecryptionResult(null, Collections.emptyList());
} }
}); });
SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath(), "--verifications-out", verifyOut.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("decrypt", "--verify-with", tempFile.getAbsolutePath(), "--verifications-out",
verifyOut.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void badDataInVerifyWithCausesExit41() throws IOException, SOPGPException.BadData { public void badDataInVerifyWithCausesExit41() throws IOException, SOPGPException.BadData {
when(decrypt.verifyWithCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(decrypt.verifyWithCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
File tempFile = File.createTempFile("verify-with-", ".tmp"); 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 @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
public void unexistentCertFileCausesExit61() { public void unexistentCertFileCausesExit61() {
SopCLI.main(new String[] {"decrypt", "--verify-with", "invalid"}); assertMissingInput(() ->
SopCLI.execute("decrypt", "--verify-with", "invalid"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE)
public void existingVerifyOutCausesExit59() throws IOException { public void existingVerifyOutCausesExit59() throws IOException {
File certFile = File.createTempFile("existing-verify-out-cert", ".asc"); File certFile = File.createTempFile("existing-verify-out-cert", ".asc");
File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp"); File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp");
SopCLI.main(new String[] {"decrypt", "--verifications-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); assertOutputExists(() -> SopCLI.execute("decrypt", "--verifications-out",
existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()));
} }
@Test @Test
@ -305,7 +324,9 @@ public class DecryptCmdTest {
} }
}); });
SopCLI.main(new String[] {"decrypt", "--verifications-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("decrypt", "--verifications-out", verifyOut.getAbsolutePath(),
"--verify-with", certFile.getAbsolutePath()));
try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) { try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) {
String line = reader.readLine(); String line = reader.readLine();
assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line); assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line);
@ -320,66 +341,64 @@ public class DecryptCmdTest {
File sessionKeyFile1 = TestFileUtil.writeTempStringFile(key1.toString()); File sessionKeyFile1 = TestFileUtil.writeTempStringFile(key1.toString());
File sessionKeyFile2 = TestFileUtil.writeTempStringFile(key2.toString()); File sessionKeyFile2 = TestFileUtil.writeTempStringFile(key2.toString());
SopCLI.main(new String[] {"decrypt", assertSuccess(() ->
"--with-session-key", sessionKeyFile1.getAbsolutePath(), SopCLI.execute("decrypt",
"--with-session-key", sessionKeyFile2.getAbsolutePath()}); "--with-session-key", sessionKeyFile1.getAbsolutePath(),
"--with-session-key", sessionKeyFile2.getAbsolutePath()));
verify(decrypt).withSessionKey(key1); verify(decrypt).withSessionKey(key1);
verify(decrypt).withSessionKey(key2); verify(decrypt).withSessionKey(key2);
} }
@Test @Test
@ExpectSystemExitWithStatus(1)
public void assertMalformedSessionKeysResultInExit1() throws IOException { public void assertMalformedSessionKeysResultInExit1() throws IOException {
File sessionKeyFile = TestFileUtil.writeTempStringFile("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137"); File sessionKeyFile = TestFileUtil.writeTempStringFile("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137");
SopCLI.main(new String[] {"decrypt", assertGenericError(() ->
"--with-session-key", sessionKeyFile.getAbsolutePath()}); SopCLI.execute("decrypt",
"--with-session-key", sessionKeyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void assertBadDataInKeysResultsInExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { public void assertBadDataInKeysResultsInExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException {
when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
File tempKeyFile = File.createTempFile("key-", ".tmp"); File tempKeyFile = File.createTempFile("key-", ".tmp");
SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); assertBadData(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
public void assertKeyFileNotFoundCausesExit61() { public void assertKeyFileNotFoundCausesExit61() {
SopCLI.main(new String[] {"decrypt", "nonexistent-key"}); assertMissingInput(() -> SopCLI.execute("decrypt", "nonexistent-key"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE)
public void assertProtectedKeyCausesExit67() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { public void assertProtectedKeyCausesExit67() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
File tempKeyFile = File.createTempFile("key-", ".tmp"); File tempKeyFile = File.createTempFile("key-", ".tmp");
SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); assertKeyIsProtected(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE)
public void assertUnsupportedAlgorithmExceptionCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { 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())); when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new IOException()));
File tempKeyFile = File.createTempFile("key-", ".tmp"); File tempKeyFile = File.createTempFile("key-", ".tmp");
SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); assertUnsupportedAsymmetricAlgo(() ->
SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
public void assertMissingPassphraseFileCausesExit61() { public void assertMissingPassphraseFileCausesExit61() {
SopCLI.main(new String[] {"decrypt", "--with-password", "missing"}); assertMissingInput(() ->
SopCLI.execute("decrypt", "--with-password", "missing"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
public void assertMissingSessionKeyFileCausesExit61() { public void assertMissingSessionKeyFileCausesExit61() {
SopCLI.main(new String[] {"decrypt", "--with-session-key", "missing"}); assertMissingInput(() ->
SopCLI.execute("decrypt", "--with-session-key", "missing"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.IncompleteVerification.EXIT_CODE)
public void verifyOutWithoutVerifyWithCausesExit23() { public void verifyOutWithoutVerifyWithCausesExit23() {
SopCLI.main(new String[] {"decrypt", "--verifications-out", "out.file"}); assertIncompleteVerification(() ->
SopCLI.execute("decrypt", "--verifications-out", "out.file"));
} }
} }

View file

@ -4,7 +4,6 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -28,6 +27,17 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
import static sop.testsuite.assertions.SopExecutionAssertions.assertCertCannotEncrypt;
import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError;
import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyCannotSign;
import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyIsProtected;
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg;
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput;
import static sop.testsuite.assertions.SopExecutionAssertions.assertPasswordNotHumanReadable;
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedAsymmetricAlgo;
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption;
public class EncryptCmdTest { public class EncryptCmdTest {
@ -50,48 +60,50 @@ public class EncryptCmdTest {
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void missingBothPasswordAndCertFileCausesMissingArg() {
public void missingBothPasswordAndCertFileCauseExit19() { assertMissingArg(() ->
SopCLI.main(new String[] {"encrypt", "--no-armor"}); SopCLI.execute("encrypt", "--no-armor"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void as_unsupportedEncryptAsCausesUnsupportedOption() throws SOPGPException.UnsupportedOption {
public void as_unsupportedEncryptAsCausesExit37() throws SOPGPException.UnsupportedOption {
when(encrypt.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting encryption mode not supported.")); 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 @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void as_invalidModeOptionCausesUnsupportedOption() {
public void as_invalidModeOptionCausesExit37() { assertUnsupportedOption(() ->
SopCLI.main(new String[] {"encrypt", "--as", "invalid"}); SopCLI.execute("encrypt", "--as", "invalid"));
} }
@Test @Test
public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption, IOException { public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption, IOException {
File passwordFile = TestFileUtil.writeTempStringFile("0rbit"); File passwordFile = TestFileUtil.writeTempStringFile("0rbit");
for (EncryptAs mode : EncryptAs.values()) { 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); verify(encrypt, times(1)).mode(mode);
} }
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.PasswordNotHumanReadable.EXIT_CODE) public void withPassword_notHumanReadablePasswordCausesPWNotHumanReadable() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
public void withPassword_notHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable()); when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable());
File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable"); File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable");
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); assertPasswordNotHumanReadable(() ->
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void withPassword_unsupportedWithPasswordCausesUnsupportedOption() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
public void withPassword_unsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported.")); when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported."));
File passwordFile = TestFileUtil.writeTempStringFile("orange"); File passwordFile = TestFileUtil.writeTempStringFile("orange");
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); assertUnsupportedOption(() ->
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath()));
} }
@Test @Test
@ -99,99 +111,107 @@ public class EncryptCmdTest {
File keyFile1 = File.createTempFile("sign-with-1-", ".asc"); File keyFile1 = File.createTempFile("sign-with-1-", ".asc");
File keyFile2 = File.createTempFile("sign-with-2-", ".asc"); File keyFile2 = File.createTempFile("sign-with-2-", ".asc");
File passwordFile = TestFileUtil.writeTempStringFile("password"); 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()); verify(encrypt, times(2)).signWith((InputStream) any());
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void signWith_nonExistentKeyFileCausesMissingInput() {
public void signWith_nonExistentKeyFileCausesExit61() { assertMissingInput(() ->
SopCLI.main(new String[] {"encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"}); SopCLI.execute("encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE) public void signWith_keyIsProtectedCausesKeyIsProtected() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
File keyFile = File.createTempFile("sign-with", ".asc"); File keyFile = File.createTempFile("sign-with", ".asc");
File passwordFile = TestFileUtil.writeTempStringFile("starship"); 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 @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) public void signWith_unsupportedAsymmetricAlgoCausesUnsupportedAsymAlgo() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception()));
File keyFile = File.createTempFile("sign-with", ".asc"); File keyFile = File.createTempFile("sign-with", ".asc");
File passwordFile = TestFileUtil.writeTempStringFile("123456"); 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 @Test
@ExpectSystemExitWithStatus(SOPGPException.KeyCannotSign.EXIT_CODE) public void signWith_certCannotSignCausesKeyCannotSign() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData {
public void signWith_certCannotSignCausesExit79() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData {
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign()); when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign());
File keyFile = File.createTempFile("sign-with", ".asc"); File keyFile = File.createTempFile("sign-with", ".asc");
File passwordFile = TestFileUtil.writeTempStringFile("dragon"); 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 @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void signWith_badDataCausesBadData() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
File keyFile = File.createTempFile("sign-with", ".asc"); File keyFile = File.createTempFile("sign-with", ".asc");
File passwordFile = TestFileUtil.writeTempStringFile("orange"); 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 @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void cert_nonExistentCertFileCausesMissingInput() {
public void cert_nonExistentCertFileCausesExit61() { assertMissingInput(() ->
SopCLI.main(new String[] {"encrypt", "invalid.asc"}); SopCLI.execute("encrypt", "invalid.asc"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) public void cert_unsupportedAsymmetricAlgorithmCausesUnsupportedAsymAlg() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
public void cert_unsupportedAsymmetricAlgorithmCausesExit13() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception()));
File certFile = File.createTempFile("cert", ".asc"); File certFile = File.createTempFile("cert", ".asc");
SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); assertUnsupportedAsymmetricAlgo(() ->
SopCLI.execute("encrypt", certFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.CertCannotEncrypt.EXIT_CODE) public void cert_certCannotEncryptCausesCertCannotEncrypt() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
public void cert_certCannotEncryptCausesExit17() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.CertCannotEncrypt("Certificate cannot encrypt.", new Exception())); when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.CertCannotEncrypt("Certificate cannot encrypt.", new Exception()));
File certFile = File.createTempFile("cert", ".asc"); File certFile = File.createTempFile("cert", ".asc");
SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); assertCertCannotEncrypt(() ->
SopCLI.execute("encrypt", certFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void cert_badDataCausesBadData() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
public void cert_badDataCausesExit41() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
File certFile = File.createTempFile("cert", ".asc"); File certFile = File.createTempFile("cert", ".asc");
SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); assertBadData(() ->
SopCLI.execute("encrypt", certFile.getAbsolutePath()));
} }
@Test @Test
public void noArmor_notCalledByDefault() throws IOException { public void noArmor_notCalledByDefault() throws IOException {
File passwordFile = TestFileUtil.writeTempStringFile("clownfish"); 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(); verify(encrypt, never()).noArmor();
} }
@Test @Test
public void noArmor_callGetsPassedDown() throws IOException { public void noArmor_callGetsPassedDown() throws IOException {
File passwordFile = TestFileUtil.writeTempStringFile("monkey"); 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(); verify(encrypt, times(1)).noArmor();
} }
@Test @Test
@ExpectSystemExitWithStatus(1) public void writeTo_ioExceptionCausesGenericError() throws IOException {
public void writeTo_ioExceptionCausesExit1() throws IOException {
when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult<EncryptionResult>() { when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult<EncryptionResult>() {
@Override @Override
public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException { public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException {
@ -199,6 +219,7 @@ public class EncryptCmdTest {
} }
}); });
File passwordFile = TestFileUtil.writeTempStringFile("wildcat"); File passwordFile = TestFileUtil.writeTempStringFile("wildcat");
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); assertGenericError(() ->
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath()));
} }
} }

View file

@ -10,12 +10,14 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sop.Ready; import sop.Ready;
@ -45,32 +47,34 @@ public class ExtractCertCmdTest {
@Test @Test
public void noArmor_notCalledByDefault() { public void noArmor_notCalledByDefault() {
SopCLI.main(new String[] {"extract-cert"}); assertSuccess(() ->
SopCLI.execute("extract-cert"));
verify(extractCert, never()).noArmor(); verify(extractCert, never()).noArmor();
} }
@Test @Test
public void noArmor_passedDown() { public void noArmor_passedDown() {
SopCLI.main(new String[] {"extract-cert", "--no-armor"}); assertSuccess(() ->
SopCLI.execute("extract-cert", "--no-armor"));
verify(extractCert, times(1)).noArmor(); verify(extractCert, times(1)).noArmor();
} }
@Test @Test
@ExpectSystemExitWithStatus(1) public void key_ioExceptionCausesGenericError() throws IOException, SOPGPException.BadData {
public void key_ioExceptionCausesExit1() throws IOException, SOPGPException.BadData {
when(extractCert.key((InputStream) any())).thenReturn(new Ready() { when(extractCert.key((InputStream) any())).thenReturn(new Ready() {
@Override @Override
public void writeTo(OutputStream outputStream) throws IOException { public void writeTo(OutputStream outputStream) throws IOException {
throw new IOException(); throw new IOException();
} }
}); });
SopCLI.main(new String[] {"extract-cert"}); assertGenericError(() ->
SopCLI.execute("extract-cert"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void key_badDataCausesBadData() throws IOException, SOPGPException.BadData {
public void key_badDataCausesExit41() throws IOException, SOPGPException.BadData {
when(extractCert.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(extractCert.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"extract-cert"}); assertBadData(() ->
SopCLI.execute("extract-cert"));
} }
} }

View file

@ -10,11 +10,14 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; 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.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InOrder; import org.mockito.InOrder;
@ -47,19 +50,22 @@ public class GenerateKeyCmdTest {
@Test @Test
public void noArmor_notCalledByDefault() { public void noArmor_notCalledByDefault() {
SopCLI.main(new String[] {"generate-key", "Alice"}); assertSuccess(() ->
SopCLI.execute("generate-key", "Alice"));
verify(generateKey, never()).noArmor(); verify(generateKey, never()).noArmor();
} }
@Test @Test
public void noArmor_passedDown() { 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(); verify(generateKey, times(1)).noArmor();
} }
@Test @Test
public void userId_multipleUserIdsPassedDownInProperOrder() { public void userId_multipleUserIdsPassedDownInProperOrder() {
SopCLI.main(new String[] {"generate-key", "Alice <alice@pgpainless.org>", "Bob <bob@pgpainless.org>"}); assertSuccess(() ->
SopCLI.execute("generate-key", "Alice <alice@pgpainless.org>", "Bob <bob@pgpainless.org>"));
InOrder inOrder = Mockito.inOrder(generateKey); InOrder inOrder = Mockito.inOrder(generateKey);
inOrder.verify(generateKey).userId("Alice <alice@pgpainless.org>"); inOrder.verify(generateKey).userId("Alice <alice@pgpainless.org>");
@ -69,30 +75,32 @@ public class GenerateKeyCmdTest {
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE)
public void missingArgumentCausesExit19() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { 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, // 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. // so we might want to change this test in the future.
when(generateKey.generate()).thenThrow(new SOPGPException.MissingArg("Missing user-id.")); when(generateKey.generate()).thenThrow(new SOPGPException.MissingArg("Missing user-id."));
SopCLI.main(new String[] {"generate-key"}); assertMissingArg(() ->
SopCLI.execute("generate-key"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE)
public void unsupportedAsymmetricAlgorithmCausesExit13() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { public void unsupportedAsymmetricAlgorithmCausesExit13() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
when(generateKey.generate()).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); 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 @Test
@ExpectSystemExitWithStatus(1) public void ioExceptionCausesGenericError() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
public void ioExceptionCausesExit1() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
when(generateKey.generate()).thenReturn(new Ready() { when(generateKey.generate()).thenReturn(new Ready() {
@Override @Override
public void writeTo(OutputStream outputStream) throws IOException { public void writeTo(OutputStream outputStream) throws IOException {
throw new IOException(); throw new IOException();
} }
}); });
SopCLI.main(new String[] {"generate-key", "Alice"});
assertGenericError(() ->
SopCLI.execute("generate-key", "Alice"));
} }
} }

View file

@ -4,7 +4,6 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sop.ReadyWithResult; import sop.ReadyWithResult;
@ -26,6 +25,8 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg;
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
public class InlineDetachCmdTest { public class InlineDetachCmdTest {
@ -41,9 +42,9 @@ public class InlineDetachCmdTest {
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void testMissingSignaturesOutResultsInMissingArg() {
public void testMissingSignaturesOutResultsInExit19() { assertMissingArg(() ->
SopCLI.main(new String[] {"inline-detach"}); SopCLI.execute("inline-detach"));
} }
@Test @Test
@ -67,7 +68,8 @@ public class InlineDetachCmdTest {
} }
}); });
SopCLI.main(new String[] {"inline-detach", "--signatures-out", tempFile.getAbsolutePath(), "--no-armor"}); assertSuccess(() ->
SopCLI.execute("inline-detach", "--signatures-out", tempFile.getAbsolutePath(), "--no-armor"));
verify(inlineDetach, times(1)).noArmor(); verify(inlineDetach, times(1)).noArmor();
verify(inlineDetach, times(1)).message((InputStream) any()); verify(inlineDetach, times(1)).message((InputStream) any());
} }

View file

@ -10,13 +10,20 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; 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.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sop.ReadyWithResult; import sop.ReadyWithResult;
@ -54,70 +61,77 @@ public class SignCmdTest {
@Test @Test
public void as_optionsAreCaseInsensitive() { public void as_optionsAreCaseInsensitive() {
SopCLI.main(new String[] {"sign", "--as", "Binary", keyFile.getAbsolutePath()}); assertSuccess(() ->
SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); SopCLI.execute("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()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void as_invalidOptionCausesExit37() { public void as_invalidOptionCausesExit37() {
SopCLI.main(new String[] {"sign", "--as", "Invalid", keyFile.getAbsolutePath()}); assertUnsupportedOption(() ->
SopCLI.execute("sign", "--as", "Invalid", keyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
when(detachedSign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting signing mode not supported.")); 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 @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
public void key_nonExistentKeyFileCausesExit61() { public void key_nonExistentKeyFileCausesExit61() {
SopCLI.main(new String[] {"sign", "invalid.asc"}); assertMissingInput(() ->
SopCLI.execute("sign", "invalid.asc"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE)
public void key_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { public void key_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData {
when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); assertKeyIsProtected(() ->
SopCLI.execute("sign", keyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData {
when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); 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 @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE)
public void key_missingKeyFileCausesExit19() { public void key_missingKeyFileCausesExit19() {
SopCLI.main(new String[] {"sign"}); assertMissingArg(() ->
SopCLI.execute("sign"));
} }
@Test @Test
public void noArmor_notCalledByDefault() { public void noArmor_notCalledByDefault() {
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("sign", keyFile.getAbsolutePath()));
verify(detachedSign, never()).noArmor(); verify(detachedSign, never()).noArmor();
} }
@Test @Test
public void noArmor_passedDown() { 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(); verify(detachedSign, times(1)).noArmor();
} }
@Test @Test
public void withKeyPassword_passedDown() { public void withKeyPassword_passedDown() {
SopCLI.main(new String[] {"sign", "--with-key-password", passFile.getAbsolutePath(), keyFile.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("sign",
"--with-key-password", passFile.getAbsolutePath(),
keyFile.getAbsolutePath()));
verify(detachedSign, times(1)).withKeyPassword("sw0rdf1sh"); verify(detachedSign, times(1)).withKeyPassword("sw0rdf1sh");
} }
@Test @Test
@ExpectSystemExitWithStatus(1)
public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText { public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText {
when(detachedSign.data((InputStream) any())).thenReturn(new ReadyWithResult<SigningResult>() { when(detachedSign.data((InputStream) any())).thenReturn(new ReadyWithResult<SigningResult>() {
@Override @Override
@ -125,13 +139,14 @@ public class SignCmdTest {
throw new IOException(); throw new IOException();
} }
}); });
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); assertGenericError(() ->
SopCLI.execute("sign", keyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.ExpectedText.EXIT_CODE)
public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText { public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText {
when(detachedSign.data((InputStream) any())).thenThrow(new SOPGPException.ExpectedText()); when(detachedSign.data((InputStream) any())).thenThrow(new SOPGPException.ExpectedText());
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); assertExpectedText(() ->
SopCLI.execute("sign", keyFile.getAbsolutePath()));
} }
} }

View file

@ -10,6 +10,11 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; 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.ByteArrayOutputStream;
import java.io.File; import java.io.File;
@ -21,7 +26,6 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -76,60 +80,75 @@ public class VerifyCmdTest {
@Test @Test
public void notAfter_passedDown() throws SOPGPException.UnsupportedOption, ParseException { public void notAfter_passedDown() throws SOPGPException.UnsupportedOption, ParseException {
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"); 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); verify(detachedVerify, times(1)).notAfter(date);
} }
@Test @Test
public void notAfter_now() throws SOPGPException.UnsupportedOption { public void notAfter_now() throws SOPGPException.UnsupportedOption {
Date now = new Date(); 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)); verify(detachedVerify, times(1)).notAfter(dateMatcher(now));
} }
@Test @Test
public void notAfter_dashCountsAsEndOfTime() throws SOPGPException.UnsupportedOption { 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); verify(detachedVerify, times(1)).notAfter(AbstractSopCmd.END_OF_TIME);
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
when(detachedVerify.notAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); 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 @Test
public void notBefore_passedDown() throws SOPGPException.UnsupportedOption, ParseException { public void notBefore_passedDown() throws SOPGPException.UnsupportedOption, ParseException {
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"); 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); verify(detachedVerify, times(1)).notBefore(date);
} }
@Test @Test
public void notBefore_now() throws SOPGPException.UnsupportedOption { public void notBefore_now() throws SOPGPException.UnsupportedOption {
Date now = new Date(); 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)); verify(detachedVerify, times(1)).notBefore(dateMatcher(now));
} }
@Test @Test
public void notBefore_dashCountsAsBeginningOfTime() throws SOPGPException.UnsupportedOption { 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); verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME);
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
when(detachedVerify.notBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); 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 @Test
public void notBeforeAndNotAfterAreCalledWithDefaultValues() throws SOPGPException.UnsupportedOption { 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)).notAfter(dateMatcher(new Date()));
verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME); verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME);
} }
@ -139,43 +158,43 @@ public class VerifyCmdTest {
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
public void cert_fileNotFoundCausesExit61() { public void cert_fileNotFoundCausesExit61() {
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), "invalid.asc"}); assertMissingInput(() ->
SopCLI.execute("verify", signature.getAbsolutePath(), "invalid.asc"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void cert_badDataCausesExit41() throws SOPGPException.BadData, IOException { public void cert_badDataCausesExit41() throws SOPGPException.BadData, IOException {
when(detachedVerify.cert((InputStream) any())).thenThrow(new SOPGPException.BadData(new 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 @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
public void signature_fileNotFoundCausesExit61() { public void signature_fileNotFoundCausesExit61() {
SopCLI.main(new String[] {"verify", "invalid.sig", cert.getAbsolutePath()}); assertMissingInput(() ->
SopCLI.execute("verify", "invalid.sig", cert.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void signature_badDataCausesExit41() throws SOPGPException.BadData, IOException { public void signature_badDataCausesExit41() throws SOPGPException.BadData, IOException {
when(detachedVerify.signatures((InputStream) any())).thenThrow(new SOPGPException.BadData(new 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 @Test
@ExpectSystemExitWithStatus(SOPGPException.NoSignature.EXIT_CODE)
public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.NoSignature()); 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 @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); 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 @Test
@ -192,7 +211,8 @@ public class VerifyCmdTest {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out)); 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); System.setOut(originalSout);

View file

@ -4,19 +4,19 @@
package sop.cli.picocli.commands; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sop.SOP; import sop.SOP;
import sop.cli.picocli.SopCLI; import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Version; 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 { public class VersionCmdTest {
private Version version; private Version version;
@ -29,6 +29,8 @@ public class VersionCmdTest {
when(version.getVersion()).thenReturn("1.0"); when(version.getVersion()).thenReturn("1.0");
when(version.getExtendedVersion()).thenReturn("MockSop Extended Version Information"); when(version.getExtendedVersion()).thenReturn("MockSop Extended Version Information");
when(version.getBackendVersion()).thenReturn("Foo"); 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); when(sop.version()).thenReturn(version);
SopCLI.setSopInstance(sop); SopCLI.setSopInstance(sop);
@ -36,26 +38,41 @@ public class VersionCmdTest {
@Test @Test
public void assertVersionCommandWorks() { public void assertVersionCommandWorks() {
SopCLI.main(new String[] {"version"}); assertSuccess(() ->
SopCLI.execute("version"));
verify(version, times(1)).getVersion(); verify(version, times(1)).getVersion();
verify(version, times(1)).getName(); verify(version, times(1)).getName();
} }
@Test @Test
public void assertExtendedVersionCommandWorks() { public void assertExtendedVersionCommandWorks() {
SopCLI.main(new String[] {"version", "--extended"}); assertSuccess(() ->
SopCLI.execute("version", "--extended"));
verify(version, times(1)).getExtendedVersion(); verify(version, times(1)).getExtendedVersion();
} }
@Test @Test
public void assertBackendVersionCommandWorks() { public void assertBackendVersionCommandWorks() {
SopCLI.main(new String[] {"version", "--backend"}); assertSuccess(() ->
SopCLI.execute("version", "--backend"));
verify(version, times(1)).getBackendVersion(); verify(version, times(1)).getBackendVersion();
} }
@Test @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() { public void assertInvalidOptionResultsInExit37() {
SopCLI.main(new String[] {"version", "--invalid"}); assertUnsupportedOption(() ->
SopCLI.execute("version", "--invalid"));
} }
} }

View file

@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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"
}

Some files were not shown because too many files have changed in this diff Show more