Compare commits

..

No commits in common. "main" and "1.2.3" have entirely different histories.
main ... 1.2.3

246 changed files with 3975 additions and 11003 deletions

View file

@ -1,29 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**Version**
<!-- What versions of the following libraries are you using? -->
- `sop-java`:
- `pgpainless-core`:
- `bouncycastle`:
**To Reproduce**
<!-- Steps to reproduce the behavior: -->
```
Example Code Block
```
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Additional context**
<!-- Add any other context about the problem here. -->

View file

@ -13,17 +13,3 @@ Source: https://pgpainless.org
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

52
.travis.yml Normal file
View file

@ -0,0 +1,52 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <info@pgpainless.org>
#
# SPDX-License-Identifier: Apache-2.0
language: java
dist: bionic
jdk:
- openjdk8
- openjdk11
services:
- docker
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.m2
before_install:
- export GRADLE_VERSION=6.2
- wget https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-all.zip
- unzip -q gradle-${GRADLE_VERSION}-all.zip
- rm gradle-${GRADLE_VERSION}-all.zip
- sudo mv gradle-${GRADLE_VERSION} /usr/local/bin/
- export PATH="/usr/local/bin/gradle-${GRADLE_VERSION}/bin:$PATH"
- docker pull fsfe/reuse:latest
- docker run -v ${TRAVIS_BUILD_DIR}:/data fsfe/reuse:latest lint
install: gradle assemble --stacktrace
# Run the test suite and also install the artifacts in the local maven
# archive to additionaly test if artifact creation is
# functional. Which hasn't always be the case in the past, see
# 90cbcaebc7a89f4f771f733a33ac9f389df85be2
# Also run javadocAll to ensure it works.
script:
- |
JAVAC_MAJOR_VERSION=$(javac -version | sed -E 's/javac ([[:digit:]]+).*/\1/')
GRADLE_TASKS=()
GRADLE_TASKS+=(check)
if [[ ${JAVAC_MAJOR_VERSION} -ge 11 ]]; then
GRADLE_TASKS+=(javadocAll)
fi
gradle ${GRADLE_TASKS[@]} --stacktrace
after_success:
- JAVAC_VERSION=$((javac -version) 2>&1)
# Only run jacocoRootReport in the Java 8 build
- if [[ "$JAVAC_VERSION" = javac\ 1.8.* ]]; then gradle jacocoRootReport coveralls; fi

View file

@ -1,17 +0,0 @@
steps:
run:
image: gradle:7.6-jdk11-jammy
commands:
# Install Sequoia-SOP
- apt update && apt install --yes sqop
# Checkout code
- git checkout $CI_COMMIT_BRANCH
# Prepare CI
- cp external-sop/src/main/resources/sop/testsuite/external/config.json.ci external-sop/src/main/resources/sop/testsuite/external/config.json
# Code works
- gradle test
# Code is clean
- gradle check javadocAll
# Code has coverage
- gradle jacocoRootReport coveralls
secrets: [coveralls_repo_token]

View file

@ -1,7 +0,0 @@
# Code is licensed properly
# See https://reuse.software/
steps:
reuse:
image: fsfe/reuse:latest
commands:
- reuse lint

View file

@ -6,141 +6,6 @@ SPDX-License-Identifier: Apache-2.0
# Changelog
## 10.0.1-SNAPSHOT
- Remove `label()` option from `Armor` operation
## 10.0.0
- Update implementation to [SOP Specification revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html).
- Throw `BadData` when passing KEYS where CERTS are expected
- Introduce `sopv` interface subset with revision `1.0`
- Add `sop version --sopv`
## 8.0.1
- `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty.
## 8.0.0
- Rewrote `sop-java` in Kotlin
- Rewrote `sop-java-picocli` in Kotlin
- Rewrote `external-sop` in Kotlin
- Update implementation to [SOP Specification revision 08](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html).
- Add `--no-armor` option to `revoke-key` and `change-key-password` subcommands
- `armor`: Deprecate `--label` option in `sop-java` and remove in `sop-java-picocli`
- `encrypt`: Add `--session-key-out` option
- Slight API changes:
- `sop.encrypt().plaintext()` now returns a `ReadyWithResult<EncryptionResult>` instead of `Ready`.
- `EncryptionResult` is a new result type, that provides access to the session key of an encrypted message
- Change `ArmorLabel` values into lowercase
- Change `EncryptAs` values into lowercase
- Change `SignAs` values into lowercase
## 7.0.0
- Update implementation to [SOP Specification revision 07](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-07.html).
- Add support for new `revoke-key` subcommand
- Add support for new `change-key-password` subcommand
- Add support for new `--signing-only` option of `generate-key` subcommand
- Add `dearmor.data(String)` utility method
- Fix typos in, and improve i18n of CLI help pages
## 6.1.0
- `listProfiles()`: Add shortcut methods `generateKey()` and `encrypt()`
- Add DSL for testing `Verification` results
- `Verification`
- Return `Optional<SignatureMode>` for `getSignatureMode()`
- Return `Optional<String>` for `getDescription()`
- `Profile`
- Add support for profiles without description
- Return `Optional<String>` for `getDescription()`
- Add `parse(String)` method for parsing profile lines
- `sop-java`: Add dependency on `com.google.code.findbugs:jsr305` for `@Nullable`, `@Nonnull` annotations
- `UTCUtil`: `parseUTCDate()` is now `@Nonnull` and throws a `ParseException` for invalid inputs
- `UTF8Util`: `decodeUTF8()` now throws `CharacterCodingException` instead of `SOPGPException.PasswordNotHumanReadable`
- `external-sop`: Properly map error codes to new exception types (ported from `5.0.1`):
- `UNSUPPORTED_PROFILE`
- `INCOMPATIBLE_OPTIONS`
## 5.0.1
- `external-sop`: Properly map error codes to new exception types:
- `UNSUPPORTED_PROFILE`
- `INCOMPATIBLE_OPTIONS`
## 6.0.0
- Update implementation to [SOP Specification revision 06](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-06.html).
- Add option `--profile=XYZ` to `encrypt` subcommand
- Add option `--sop-spec` to `version` subcommand
- `Version`: Add different getters for specification-related values
## 5.0.0
- Update implementation to [SOP Specification revision 05](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html).
- Add the concept of profiles
- Add `list-profiles` subcommand
- Add option `--profile=XYZ` to `generate-key` subcommand
- `Verification` objects can now optionally indicate the type of the signature (`mode:text` or `mode:binary`)
- `Verification` objects can now contain an optional description of the signature
- `inline-sign` now throws an error if incompatible options `--as=clearsigned` and `--no-armor` are used
## 4.1.1
- Restructure test suite to allow simultaneous testing of multiple backends
- Fix IOException in `sop sign` due to premature stream closing
- Allow for downstream implementations of `sop-java` to reuse the test suite
- Check out Javadoc of `sop-java/src/testFixtures/java/sop/testsuite/SOPInstanceFactory` for details
## 4.1.0
- Add module `external-sop`
- This module implements the `sop-java` interfaces and allows the use of an external SOP binary
- `decrypt`: Rename `--not-before`, `--not-after` to `--verify-not-before`, `--verify-not-after`
- `decrypt`: Throw `NoSignature` error if no verifiable signature found, but signature verification is requested using `--verify-with`.
- `inline-sign`: Fix parameter label of `--as=clearsigned`
- `ArmorLabel`, `EncryptAs`, `SignAs`: make `toString()` return lowercase
## 4.0.7
- Make i18n string for `--stacktrace` option translatable
- Make manpages generation reproducible
- `dearmor`: Transform `IOException` into `BadData`
## 4.0.6
- Add support for file descriptors on unix / linux systems
## 4.0.5
- `inline-sign`: Make possible values of `--as` option lowercase
- `inline-sign`: Rename value `cleartextsigned` of option `--as` to `clearsigned`
## 4.0.4
- Not found
## 4.0.3
- `decrypt`: Rename option `--verify-out` to `--verifications-out`, but keep `--verify-out` as alias
- Fix: `decrypt`: Flush output stream in order to prevent empty file as result of `--session-key-out`
- Fix: Properly format session key for `--session-key-out`
- Be less finicky about input session key formats
- Allow upper- and lowercase hexadecimal keys
- Allow trailing whitespace
## 4.0.2
- Fix: `verify`: Do not include detached signature in list of certificates
- Fix: `inline-verify`: Also include the first argument in list of certificates
- Hide stacktraces by default and add `--stacktrace` option to print them
- Properly throw `CannotDecrypt` exception when message could not be decrypted
## 4.0.1
- Use shared resources for i18n
- Fix strings not being resolved properly when downstream renames `sop` command
## 4.0.0
- Switch to new versioning format to indicate implemented SOP version
- Implement SOP specification version 04
- Add `--with-key-password` to `sop generate-key`
- Add `--with-key-password` to `sop sign`
- Add `--with-key-password` to `sop encrypt`
- Add `--with-key-password` to `sop decrypt`
- Rename `sop detach-inband-signature-and-message` to `sop inline-detach`
- `sop inline-detach`: Add support for inline-signed messages
- Implement `sop inline-sign`
- Implement `sop inline-verify`
- Rename `Sign` to `DetachedSign`
- Rename `Verify` to `DetachedVerify`
- `SignAs`: Remove `Mime` option
- `sop-java-picocli`: Implement i18n and add German translation
## 1.2.3
- Bump Mockito version to `4.5.1`

View file

@ -6,18 +6,16 @@ SPDX-License-Identifier: Apache-2.0
# SOP for Java
[![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java)
[![Spec Revision: 10](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/10/)
[![Coverage Status](https://coveralls.io/repos/github/pgpainless/sop-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/sop-java?branch=main)
[![Travis (.com)](https://travis-ci.com/pgpainless/sop-java.svg?branch=master)](https://travis-ci.com/pgpainless/sop-java)
[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/sop-java)](https://search.maven.org/artifact/org.pgpainless/sop-java)
[![Spec Revision: 3](https://img.shields.io/badge/Spec%20Revision-3-blue)](https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03)
[![Coverage Status](https://coveralls.io/repos/github/pgpainless/sop-java/badge.svg?branch=master)](https://coveralls.io/github/pgpainless/sop-java?branch=master)
[![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/sop-java)](https://api.reuse.software/info/github.com/pgpainless/sop-java)
The [Stateless OpenPGP Protocol](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) specification
The [Stateless OpenPGP Protocol](https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03) specification
defines a generic stateless CLI for dealing with OpenPGP messages.
Its goal is to provide a minimal, yet powerful API for the most common OpenPGP related operations.
[![Packaging status](https://repology.org/badge/vertical-allrepos/sop-java.svg)](https://repology.org/project/pgpainless/versions)
[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/sop-java)](https://search.maven.org/artifact/org.pgpainless/sop-java)
## Modules
The repository contains the following modules:
@ -25,19 +23,16 @@ The repository contains the following modules:
* [sop-java](/sop-java) defines a set of Java interfaces describing the Stateless OpenPGP Protocol.
* [sop-java-picocli](/sop-java-picocli) contains a wrapper application that transforms the `sop-java` API into a command line application
compatible with the SOP-CLI specification.
* [external-sop](/external-sop) contains an API implementation that can be used to forward API calls to a SOP executable,
allowing to delegate the implementation logic to an arbitrary SOP CLI implementation.
## Known Implementations
(Please expand!)
| Project | Description |
|-------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| [pgpainless-sop](https://github.com/pgpainless/pgpainless/tree/main/pgpainless-sop) | Implementation of `sop-java` using PGPainless |
| [external-sop](https://github.com/pgpainless/sop-java/tree/main/external-sop) | Implementation of `sop-java` that allows binding to external SOP binaries such as `sqop` |
| Project | Description |
|---------------------------------------------------------------------------------------|-----------------------------------------------|
| [pgpainless-sop](https://github.com/pgpainless/pgpainless/tree/master/pgpainless-sop) | Implementation of `sop-java` using PGPainless |
### Implementations in other languages
| Project | Language |
|-------------------------------------------------|----------|
| [sop-rs](https://sequoia-pgp.gitlab.io/sop-rs/) | Rust |
| [SOP for python](https://pypi.org/project/sop/) | Python |
| [SOP for python](https://pypi.org/project/sop/) | Python |

View file

@ -19,8 +19,6 @@ buildscript {
plugins {
id 'ru.vyarus.animalsniffer' version '1.5.3'
id 'org.jetbrains.kotlin.jvm' version "1.8.10"
id 'com.diffplug.spotless' version '6.22.0' apply false
}
apply from: 'version.gradle'
@ -31,8 +29,6 @@ allprojects {
apply plugin: 'eclipse'
apply plugin: 'jacoco'
apply plugin: 'checkstyle'
apply plugin: 'kotlin'
apply plugin: 'com.diffplug.spotless'
// For non-cli modules enable android api compatibility check
if (it.name.equals('sop-java')) {
@ -57,12 +53,6 @@ allprojects {
toolVersion = '8.18'
}
spotless {
kotlin {
ktfmt().dropboxStyle()
}
}
group 'org.pgpainless'
description = "Stateless OpenPGP Protocol API for Java"
version = shortVersion
@ -79,13 +69,6 @@ allprojects {
reproducibleFileOrder = true
}
// Compatibility of default implementations in kotlin interfaces with Java implementations.
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += ["-Xjvm-default=all-compatibility"]
}
}
project.ext {
rootConfigDir = new File(rootDir, 'config')
gitCommit = getGitCommit()

View file

@ -1,59 +0,0 @@
<!--
SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
SPDX-License-Identifier: Apache-2.0
-->
# External-SOP
Access an external SOP binary from within your Java/Kotlin application.
This module implements a backend for `sop-java` that binds to external SOP binaries (such as
[sqop](https://gitlab.com/sequoia-pgp/sequoia-sop/), [python-sop](https://pypi.org/project/sop/) etc.).
SOP operation calls will be delegated to the external binary, and the results are parsed back, so that you can
access them from your Java application as usual.
## Example
Let's say you are using `ExampleSOP` which is a binary installed in `/usr/bin/example-sop`.
Instantiating a `SOP` object is as simple as this:
```java
SOP sop = new ExternalSOP("/usr/bin/example-sop");
```
This SOP object can now be used as usual (see [here](../sop-java/README.md)).
Keep in mind the license of the external SOP binary when integrating one with your project!
Some SOP binaries might require additional configuration, e.g. a Java based SOP might need to know which JAVA_HOME to use.
For this purpose, additional environment variables can be passed in using a `Properties` object:
```java
Properties properties = new Properties();
properties.put("JAVA_HOME", "/usr/lib/jvm/[...]");
SOP sop = new ExternalSOP("/usr/bin/example-sop", properties);
```
Most results of SOP operations are communicated via standard-out, standard-in. However, some operations rely on
writing results to additional output files.
To handle such results, we need to provide a temporary directory, to which those results can be written by the SOP,
and from which `External-SOP` reads them back.
The default implementation relies on `Files.createTempDirectory()` to provide a temporary directory.
It is however possible to overwrite this behavior, in order to specify a custom, perhaps more private directory:
```java
ExternalSOP.TempDirProvider provider = new ExternalSOP.TempDirProvider() {
@Override
public File provideTempDirectory() throws IOException {
File myTempDir = new File("/path/to/directory");
myTempDir.mkdirs();
return myTempDir;
}
};
SOP sop = new ExternalSOP("/usr/bin/example-sop", provider);
```
## Testing
The `external-sop` module comes with a growing test suite, which tests SOP binaries against the expectations of the SOP specification.
To configure one or multiple backends for use with the test suite, just provide a custom `config.json` file in `src/main/resources/sop/external`.
An example configuration file with the required file format is available as `config.json.example`.

View file

@ -1,43 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
plugins {
id 'java-library'
}
group 'org.pgpainless'
repositories {
mavenCentral()
}
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
api project(":sop-java")
api "org.slf4j:slf4j-api:$slf4jVersion"
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
// @Nonnull, @Nullable...
implementation "com.google.code.findbugs:jsr305:$jsrVersion"
// The ExternalTestSubjectFactory reads json config file to find configured SOP binaries...
testImplementation "com.google.code.gson:gson:$gsonVersion"
// ...and extends TestSubjectFactory
testImplementation(testFixtures(project(":sop-java")))
}
test {
// Inject configured external SOP instances using our custom TestSubjectFactory
environment("test.implementation", "sop.testsuite.external.ExternalSOPInstanceFactory")
useJUnitPlatform()
// since we test external backends which we might not control,
// we ignore test failures in this module
ignoreFailures = true
}

View file

@ -1,318 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external
import java.io.*
import java.nio.file.Files
import java.util.*
import javax.annotation.Nonnull
import sop.Ready
import sop.SOP
import sop.exception.SOPGPException.*
import sop.external.ExternalSOP.TempDirProvider
import sop.external.operation.*
import sop.operation.*
/**
* Implementation of the [SOP] API using an external SOP binary.
*
* Instantiate an [ExternalSOP] object for the given binary and the given [TempDirProvider] using
* empty environment variables.
*
* @param binaryName name / path of the SOP binary
* @param tempDirProvider custom tempDirProvider
*/
class ExternalSOP(
private val binaryName: String,
private val properties: Properties = Properties(),
private val tempDirProvider: TempDirProvider = defaultTempDirProvider()
) : SOP {
constructor(
binaryName: String,
properties: Properties
) : this(binaryName, properties, defaultTempDirProvider())
override fun version(): Version = VersionExternal(binaryName, properties)
override fun generateKey(): GenerateKey = GenerateKeyExternal(binaryName, properties)
override fun extractCert(): ExtractCert = ExtractCertExternal(binaryName, properties)
override fun detachedSign(): DetachedSign =
DetachedSignExternal(binaryName, properties, tempDirProvider)
override fun inlineSign(): InlineSign = InlineSignExternal(binaryName, properties)
override fun detachedVerify(): DetachedVerify = DetachedVerifyExternal(binaryName, properties)
override fun inlineVerify(): InlineVerify =
InlineVerifyExternal(binaryName, properties, tempDirProvider)
override fun inlineDetach(): InlineDetach =
InlineDetachExternal(binaryName, properties, tempDirProvider)
override fun encrypt(): Encrypt = EncryptExternal(binaryName, properties, tempDirProvider)
override fun decrypt(): Decrypt = DecryptExternal(binaryName, properties, tempDirProvider)
override fun armor(): Armor = ArmorExternal(binaryName, properties)
override fun dearmor(): Dearmor = DearmorExternal(binaryName, properties)
override fun listProfiles(): ListProfiles = ListProfilesExternal(binaryName, properties)
override fun revokeKey(): RevokeKey = RevokeKeyExternal(binaryName, properties)
override fun changeKeyPassword(): ChangeKeyPassword =
ChangeKeyPasswordExternal(binaryName, properties)
/**
* This interface can be used to provide a directory in which external SOP binaries can
* temporarily store additional results of OpenPGP operations such that the binding classes can
* parse them out from there. Unfortunately, on Java you cannot open
* [FileDescriptors][java.io.FileDescriptor] arbitrarily, so we have to rely on temporary files
* to pass results. An example: `sop decrypt` can emit signature verifications via
* `--verify-out=/path/to/tempfile`. [DecryptExternal] will then parse the temp file to make the
* result available to consumers. Temporary files are deleted after being read, yet creating
* temp files for sensitive information on disk might pose a security risk. Use with care!
*/
fun interface TempDirProvider {
@Throws(IOException::class) fun provideTempDirectory(): File
}
companion object {
@JvmStatic
@Throws(IOException::class)
fun finish(process: Process) {
try {
mapExitCodeOrException(process)
} catch (e: InterruptedException) {
throw RuntimeException(e)
}
}
@JvmStatic
@Throws(InterruptedException::class, IOException::class)
private fun mapExitCodeOrException(process: Process) {
// wait for process termination
val exitCode = process.waitFor()
if (exitCode == 0) {
// we're good, bye
return
}
// Read error message
val errIn = process.errorStream
val errorMessage = readString(errIn)
when (exitCode) {
NoSignature.EXIT_CODE ->
throw NoSignature(
"External SOP backend reported error NoSignature ($exitCode):\n$errorMessage")
UnsupportedAsymmetricAlgo.EXIT_CODE ->
throw UnsupportedOperationException(
"External SOP backend reported error UnsupportedAsymmetricAlgo ($exitCode):\n$errorMessage")
CertCannotEncrypt.EXIT_CODE ->
throw CertCannotEncrypt(
"External SOP backend reported error CertCannotEncrypt ($exitCode):\n$errorMessage")
MissingArg.EXIT_CODE ->
throw MissingArg(
"External SOP backend reported error MissingArg ($exitCode):\n$errorMessage")
IncompleteVerification.EXIT_CODE ->
throw IncompleteVerification(
"External SOP backend reported error IncompleteVerification ($exitCode):\n$errorMessage")
CannotDecrypt.EXIT_CODE ->
throw CannotDecrypt(
"External SOP backend reported error CannotDecrypt ($exitCode):\n$errorMessage")
PasswordNotHumanReadable.EXIT_CODE ->
throw PasswordNotHumanReadable(
"External SOP backend reported error PasswordNotHumanReadable ($exitCode):\n$errorMessage")
UnsupportedOption.EXIT_CODE ->
throw UnsupportedOption(
"External SOP backend reported error UnsupportedOption ($exitCode):\n$errorMessage")
BadData.EXIT_CODE ->
throw BadData(
"External SOP backend reported error BadData ($exitCode):\n$errorMessage")
ExpectedText.EXIT_CODE ->
throw ExpectedText(
"External SOP backend reported error ExpectedText ($exitCode):\n$errorMessage")
OutputExists.EXIT_CODE ->
throw OutputExists(
"External SOP backend reported error OutputExists ($exitCode):\n$errorMessage")
MissingInput.EXIT_CODE ->
throw MissingInput(
"External SOP backend reported error MissingInput ($exitCode):\n$errorMessage")
KeyIsProtected.EXIT_CODE ->
throw KeyIsProtected(
"External SOP backend reported error KeyIsProtected ($exitCode):\n$errorMessage")
UnsupportedSubcommand.EXIT_CODE ->
throw UnsupportedSubcommand(
"External SOP backend reported error UnsupportedSubcommand ($exitCode):\n$errorMessage")
UnsupportedSpecialPrefix.EXIT_CODE ->
throw UnsupportedSpecialPrefix(
"External SOP backend reported error UnsupportedSpecialPrefix ($exitCode):\n$errorMessage")
AmbiguousInput.EXIT_CODE ->
throw AmbiguousInput(
"External SOP backend reported error AmbiguousInput ($exitCode):\n$errorMessage")
KeyCannotSign.EXIT_CODE ->
throw KeyCannotSign(
"External SOP backend reported error KeyCannotSign ($exitCode):\n$errorMessage")
IncompatibleOptions.EXIT_CODE ->
throw IncompatibleOptions(
"External SOP backend reported error IncompatibleOptions ($exitCode):\n$errorMessage")
UnsupportedProfile.EXIT_CODE ->
throw UnsupportedProfile(
"External SOP backend reported error UnsupportedProfile ($exitCode):\n$errorMessage")
// Did you forget to add a case for a new exception type?
else ->
throw RuntimeException(
"External SOP backend reported unknown exit code ($exitCode):\n$errorMessage")
}
}
/**
* Return all key-value pairs from the given [Properties] object as a list with items of the
* form `key=value`.
*
* @param properties properties
* @return list of key=value strings
*/
@JvmStatic
fun propertiesToEnv(properties: Properties): List<String> =
properties.map { "${it.key}=${it.value}" }
/**
* Read the contents of the [InputStream] and return them as a [String].
*
* @param inputStream input stream
* @return string
* @throws IOException in case of an IO error
*/
@JvmStatic
@Throws(IOException::class)
fun readString(inputStream: InputStream): String {
val bOut = ByteArrayOutputStream()
val buf = ByteArray(4096)
var r: Int
while (inputStream.read(buf).also { r = it } > 0) {
bOut.write(buf, 0, r)
}
return bOut.toString()
}
/**
* Execute the given command on the given [Runtime] with the given list of environment
* variables. This command does not transform any input data, and instead is purely a
* producer.
*
* @param runtime runtime
* @param commandList command
* @param envList environment variables
* @return ready to read the result from
*/
@JvmStatic
fun executeProducingOperation(
runtime: Runtime,
commandList: List<String>,
envList: List<String>
): Ready {
try {
val process = runtime.exec(commandList.toTypedArray(), envList.toTypedArray())
val stdIn = process.inputStream
return object : Ready() {
@Throws(IOException::class)
override fun writeTo(@Nonnull outputStream: OutputStream) {
val buf = ByteArray(4096)
var r: Int
while (stdIn.read(buf).also { r = it } >= 0) {
outputStream.write(buf, 0, r)
}
outputStream.flush()
outputStream.close()
finish(process)
}
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
/**
* Execute the given command on the given runtime using the given environment variables. The
* given input stream provides input for the process. This command is a transformation,
* meaning it is given input data and transforms it into output data.
*
* @param runtime runtime
* @param commandList command
* @param envList environment variables
* @param standardIn stream of input data for the process
* @return ready to read the result from
*/
@JvmStatic
fun executeTransformingOperation(
runtime: Runtime,
commandList: List<String>,
envList: List<String>,
standardIn: InputStream
): Ready {
try {
val process = runtime.exec(commandList.toTypedArray(), envList.toTypedArray())
val processOut = process.outputStream
val processIn = process.inputStream
return object : Ready() {
override fun writeTo(outputStream: OutputStream) {
val buf = ByteArray(4096)
var r: Int
while (standardIn.read(buf).also { r = it } > 0) {
processOut.write(buf, 0, r)
}
standardIn.close()
try {
processOut.flush()
processOut.close()
} catch (e: IOException) {
// Perhaps the stream is already closed, in which case we ignore the
// exception.
if ("Stream closed" != e.message) {
throw e
}
}
while (processIn.read(buf).also { r = it } > 0) {
outputStream.write(buf, 0, r)
}
processIn.close()
outputStream.flush()
outputStream.close()
finish(process)
}
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
/**
* Default implementation of the [TempDirProvider] which stores temporary files in the
* systems temp dir ([Files.createTempDirectory]).
*
* @return default implementation
*/
@JvmStatic
fun defaultTempDirProvider(): TempDirProvider {
return TempDirProvider { Files.createTempDirectory("ext-sop").toFile() }
}
}
}

View file

@ -1,53 +0,0 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external
import java.nio.file.Files
import java.util.*
import sop.SOPV
import sop.external.ExternalSOP.TempDirProvider
import sop.external.operation.DetachedVerifyExternal
import sop.external.operation.InlineVerifyExternal
import sop.external.operation.VersionExternal
import sop.operation.DetachedVerify
import sop.operation.InlineVerify
import sop.operation.Version
/**
* Implementation of the [SOPV] API subset using an external sopv/sop binary.
*
* Instantiate an [ExternalSOPV] object for the given binary and the given [TempDirProvider] using
* empty environment variables.
*
* @param binaryName name / path of the sopv binary
* @param tempDirProvider custom tempDirProvider
*/
class ExternalSOPV(
private val binaryName: String,
private val properties: Properties = Properties(),
private val tempDirProvider: TempDirProvider = defaultTempDirProvider()
) : SOPV {
override fun version(): Version = VersionExternal(binaryName, properties)
override fun detachedVerify(): DetachedVerify = DetachedVerifyExternal(binaryName, properties)
override fun inlineVerify(): InlineVerify =
InlineVerifyExternal(binaryName, properties, tempDirProvider)
companion object {
/**
* Default implementation of the [TempDirProvider] which stores temporary files in the
* systems temp dir ([Files.createTempDirectory]).
*
* @return default implementation
*/
@JvmStatic
fun defaultTempDirProvider(): TempDirProvider {
return TempDirProvider { Files.createTempDirectory("ext-sopv").toFile() }
}
}
}

View file

@ -1,23 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.InputStream
import java.util.Properties
import sop.Ready
import sop.exception.SOPGPException
import sop.external.ExternalSOP
import sop.operation.Armor
/** Implementation of the [Armor] operation using an external SOP binary. */
class ArmorExternal(binary: String, environment: Properties) : Armor {
private val commandList: MutableList<String> = mutableListOf(binary, "armor")
private val envList: List<String> = ExternalSOP.propertiesToEnv(environment)
@Throws(SOPGPException.BadData::class)
override fun data(data: InputStream): Ready =
ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data)
}

View file

@ -1,37 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.InputStream
import java.util.Properties
import sop.Ready
import sop.external.ExternalSOP
import sop.operation.ChangeKeyPassword
/** Implementation of the [ChangeKeyPassword] operation using an external SOP binary. */
class ChangeKeyPasswordExternal(binary: String, environment: Properties) : ChangeKeyPassword {
private val commandList: MutableList<String> = mutableListOf(binary, "change-key-password")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var keyPasswordCounter = 0
override fun noArmor(): ChangeKeyPassword = apply { commandList.add("--no-armor") }
override fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword = apply {
commandList.add("--old-key-password=@ENV:KEY_PASSWORD_$keyPasswordCounter")
envList.add("KEY_PASSWORD_$keyPasswordCounter=$oldPassphrase")
keyPasswordCounter += 1
}
override fun newKeyPassphrase(newPassphrase: String): ChangeKeyPassword = apply {
commandList.add("--new-key-password=@ENV:KEY_PASSWORD_$keyPasswordCounter")
envList.add("KEY_PASSWORD_$keyPasswordCounter=$newPassphrase")
keyPasswordCounter += 1
}
override fun keys(keys: InputStream): Ready =
ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys)
}

View file

@ -1,20 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.InputStream
import java.util.Properties
import sop.Ready
import sop.external.ExternalSOP
import sop.operation.Dearmor
/** Implementation of the [Dearmor] operation using an external SOP binary. */
class DearmorExternal(binary: String, environment: Properties) : Dearmor {
private val commandList = listOf(binary, "dearmor")
private val envList = ExternalSOP.propertiesToEnv(environment)
override fun data(data: InputStream): Ready =
ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data)
}

View file

@ -1,133 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.*
import java.util.*
import sop.DecryptionResult
import sop.ReadyWithResult
import sop.SessionKey
import sop.Verification
import sop.external.ExternalSOP
import sop.external.ExternalSOP.Companion.finish
import sop.external.ExternalSOP.Companion.readString
import sop.operation.Decrypt
import sop.util.UTCUtil
/** Implementation of the [Decrypt] operation using an external SOP binary. */
class DecryptExternal(
binary: String,
environment: Properties,
private val tempDirProvider: ExternalSOP.TempDirProvider
) : Decrypt {
private val commandList = mutableListOf(binary, "decrypt")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var argCounter = 0
private var requireVerification = false
override fun verifyNotBefore(timestamp: Date): Decrypt = apply {
commandList.add("--verify-not-before=${UTCUtil.formatUTCDate(timestamp)}")
}
override fun verifyNotAfter(timestamp: Date): Decrypt = apply {
commandList.add("--verify-not-after=${UTCUtil.formatUTCDate(timestamp)}")
}
override fun verifyWithCert(cert: InputStream): Decrypt = apply {
commandList.add("--verify-with=@ENV:VERIFY_WITH_$argCounter")
envList.add("VERIFY_WITH_$argCounter=${readString(cert)}")
argCounter += 1
requireVerification = true
}
override fun withSessionKey(sessionKey: SessionKey): Decrypt = apply {
commandList.add("--with-session-key=@ENV:SESSION_KEY_$argCounter")
envList.add("SESSION_KEY_$argCounter=$sessionKey")
argCounter += 1
}
override fun withPassword(password: String): Decrypt = apply {
commandList.add("--with-password=@ENV:PASSWORD_$argCounter")
envList.add("PASSWORD_$argCounter=$password")
argCounter += 1
}
override fun withKey(key: InputStream): Decrypt = apply {
commandList.add("@ENV:KEY_$argCounter")
envList.add("KEY_$argCounter=${readString(key)}")
argCounter += 1
}
override fun withKeyPassword(password: ByteArray): Decrypt = apply {
commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCounter")
envList.add("KEY_PASSWORD_$argCounter=${String(password)}")
argCounter += 1
}
override fun ciphertext(ciphertext: InputStream): ReadyWithResult<DecryptionResult> {
val tempDir = tempDirProvider.provideTempDirectory()
val sessionKeyOut = File(tempDir, "session-key-out")
sessionKeyOut.delete()
commandList.add("--session-key-out=${sessionKeyOut.absolutePath}")
val verifyOut = File(tempDir, "verifications-out")
verifyOut.delete()
if (requireVerification) {
commandList.add("--verify-out=${verifyOut.absolutePath}")
}
try {
val process =
Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray())
val processOut = process.outputStream
val processIn = process.inputStream
return object : ReadyWithResult<DecryptionResult>() {
override fun writeTo(outputStream: OutputStream): DecryptionResult {
val buf = ByteArray(4096)
var r: Int
while (ciphertext.read(buf).also { r = it } > 0) {
processOut.write(buf, 0, r)
}
ciphertext.close()
processOut.close()
while (processIn.read(buf).also { r = it } > 0) {
outputStream.write(buf, 0, r)
}
processIn.close()
outputStream.close()
finish(process)
val sessionKeyOutIn = FileInputStream(sessionKeyOut)
var line = readString(sessionKeyOutIn)
val sessionKey = SessionKey.fromString(line.trim { it <= ' ' })
sessionKeyOutIn.close()
sessionKeyOut.delete()
val verifications: MutableList<Verification> = ArrayList()
if (requireVerification) {
val verifyOutIn = FileInputStream(verifyOut)
val reader = BufferedReader(InputStreamReader(verifyOutIn))
while (reader.readLine().also { line = it } != null) {
verifications.add(Verification.fromString(line.trim()))
}
reader.close()
}
return DecryptionResult(sessionKey, verifications)
}
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -1,104 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.*
import java.util.*
import sop.MicAlg
import sop.ReadyWithResult
import sop.SigningResult
import sop.SigningResult.Companion.builder
import sop.enums.SignAs
import sop.external.ExternalSOP
import sop.external.ExternalSOP.Companion.finish
import sop.operation.DetachedSign
/** Implementation of the [DetachedSign] operation using an external SOP binary. */
class DetachedSignExternal(
binary: String,
environment: Properties,
private val tempDirProvider: ExternalSOP.TempDirProvider
) : DetachedSign {
private val commandList = mutableListOf(binary, "sign")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var argCounter = 0
override fun mode(mode: SignAs): DetachedSign = apply { commandList.add("--as=$mode") }
override fun data(data: InputStream): ReadyWithResult<SigningResult> {
val tempDir = tempDirProvider.provideTempDirectory()
val micAlgOut = File(tempDir, "micAlgOut")
micAlgOut.delete()
commandList.add("--micalg-out=${micAlgOut.absolutePath}")
try {
val process =
Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray())
val processOut = process.outputStream
val processIn = process.inputStream
return object : ReadyWithResult<SigningResult>() {
override fun writeTo(outputStream: OutputStream): SigningResult {
val buf = ByteArray(4096)
var r: Int
while (data.read(buf).also { r = it } > 0) {
processOut.write(buf, 0, r)
}
data.close()
try {
processOut.close()
} catch (e: IOException) {
// Ignore Stream closed
if ("Stream closed" != e.message) {
throw e
}
}
while (processIn.read(buf).also { r = it } > 0) {
outputStream.write(buf, 0, r)
}
processIn.close()
outputStream.close()
finish(process)
val builder = builder()
if (micAlgOut.exists()) {
val reader = BufferedReader(InputStreamReader(FileInputStream(micAlgOut)))
val line = reader.readLine()
if (line != null && line.isNotBlank()) {
val micAlg = MicAlg(line.trim())
builder.setMicAlg(micAlg)
}
reader.close()
micAlgOut.delete()
}
return builder.build()
}
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
override fun noArmor(): DetachedSign = apply { commandList.add("--no-armor") }
override fun key(key: InputStream): DetachedSign = apply {
commandList.add("@ENV:KEY_$argCounter")
envList.add("KEY_$argCounter=${ExternalSOP.readString(key)}")
argCounter += 1
}
override fun withKeyPassword(password: ByteArray): DetachedSign = apply {
commandList.add("--with-key-password=@ENV:WITH_KEY_PASSWORD_$argCounter")
envList.add("WITH_KEY_PASSWORD_$argCounter=${String(password)}")
argCounter += 1
}
}

View file

@ -1,90 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.util.*
import sop.Verification
import sop.Verification.Companion.fromString
import sop.exception.SOPGPException
import sop.external.ExternalSOP
import sop.external.ExternalSOP.Companion.finish
import sop.operation.DetachedVerify
import sop.operation.VerifySignatures
import sop.util.UTCUtil
/** Implementation of the [DetachedVerify] operation using an external SOP binary. */
class DetachedVerifyExternal(binary: String, environment: Properties) : DetachedVerify {
private val commandList = mutableListOf(binary, "verify")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var signatures: InputStream? = null
private val certs: MutableSet<InputStream> = mutableSetOf()
private var argCounter = 0
override fun signatures(signatures: InputStream): VerifySignatures = apply {
this.signatures = signatures
}
override fun notBefore(timestamp: Date): DetachedVerify = apply {
commandList.add("--not-before=${UTCUtil.formatUTCDate(timestamp)}")
}
override fun notAfter(timestamp: Date): DetachedVerify = apply {
commandList.add("--not-after=${UTCUtil.formatUTCDate(timestamp)}")
}
override fun cert(cert: InputStream): DetachedVerify = apply { this.certs.add(cert) }
override fun data(data: InputStream): List<Verification> {
// Signature
if (signatures == null) {
throw SOPGPException.MissingArg("Missing argument: signatures cannot be null.")
}
commandList.add("@ENV:SIGNATURE")
envList.add("SIGNATURE=${ExternalSOP.readString(signatures!!)}")
// Certs
for (cert in certs) {
commandList.add("@ENV:CERT_$argCounter")
envList.add("CERT_$argCounter=${ExternalSOP.readString(cert)}")
argCounter += 1
}
try {
val process =
Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray())
val processOut = process.outputStream
val processIn = process.inputStream
val buf = ByteArray(4096)
var r: Int
while (data.read(buf).also { r = it } > 0) {
processOut.write(buf, 0, r)
}
data.close()
processOut.close()
val bufferedReader = BufferedReader(InputStreamReader(processIn))
val verifications: MutableList<Verification> = ArrayList()
var line: String?
while (bufferedReader.readLine().also { line = it } != null) {
verifications.add(fromString(line!!))
}
finish(process)
return verifications
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -1,111 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.util.*
import sop.EncryptionResult
import sop.ReadyWithResult
import sop.SessionKey.Companion.fromString
import sop.enums.EncryptAs
import sop.external.ExternalSOP
import sop.external.ExternalSOP.Companion.finish
import sop.external.ExternalSOP.Companion.readString
import sop.operation.Encrypt
/** Implementation of the [Encrypt] operation using an external SOP binary. */
class EncryptExternal(
binary: String,
environment: Properties,
private val tempDirProvider: ExternalSOP.TempDirProvider
) : Encrypt {
private val commandList = mutableListOf(binary, "encrypt")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var argCounter = 0
override fun noArmor(): Encrypt = apply { commandList.add("--no-armor") }
override fun mode(mode: EncryptAs): Encrypt = apply { commandList.add("--as=$mode") }
override fun signWith(key: InputStream): Encrypt = apply {
commandList.add("--sign-with@ENV:SIGN_WITH_$argCounter")
envList.add("SIGN_WITH_$argCounter=${ExternalSOP.readString(key)}")
argCounter += 1
}
override fun withKeyPassword(password: ByteArray): Encrypt = apply {
commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCounter")
envList.add("KEY_PASSWORD_$argCounter=${String(password)}")
argCounter += 1
}
override fun withPassword(password: String): Encrypt = apply {
commandList.add("--with-password=@ENV:PASSWORD_$argCounter")
envList.add("PASSWORD_$argCounter=$password")
argCounter += 1
}
override fun withCert(cert: InputStream): Encrypt = apply {
commandList.add("@ENV:CERT_$argCounter")
envList.add("CERT_$argCounter=${readString(cert)}")
argCounter += 1
}
override fun profile(profileName: String): Encrypt = apply {
commandList.add("--profile=$profileName")
}
override fun plaintext(plaintext: InputStream): ReadyWithResult<EncryptionResult> {
val tempDir = tempDirProvider.provideTempDirectory()
val sessionKeyOut = File(tempDir, "session-key-out")
sessionKeyOut.delete()
commandList.add("--session-key-out=${sessionKeyOut.absolutePath}")
try {
val process =
Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray())
val processOut = process.outputStream
val processIn = process.inputStream
return object : ReadyWithResult<EncryptionResult>() {
override fun writeTo(outputStream: OutputStream): EncryptionResult {
val buf = ByteArray(4096)
var r: Int
while (plaintext.read(buf).also { r = it } > 0) {
processOut.write(buf, 0, r)
}
plaintext.close()
processOut.close()
while (processIn.read(buf).also { r = it } > 0) {
outputStream.write(buf, 0, r)
}
processIn.close()
outputStream.close()
finish(process)
val sessionKeyOutIn = FileInputStream(sessionKeyOut)
val line = readString(sessionKeyOutIn)
val sessionKey = fromString(line.trim())
sessionKeyOutIn.close()
sessionKeyOut.delete()
return EncryptionResult(sessionKey)
}
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -1,24 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.InputStream
import java.util.Properties
import sop.Ready
import sop.external.ExternalSOP
import sop.operation.ExtractCert
/** Implementation of the [ExtractCert] operation using an external SOP binary. */
class ExtractCertExternal(binary: String, environment: Properties) : ExtractCert {
private val commandList = mutableListOf(binary, "extract-cert")
private val envList = ExternalSOP.propertiesToEnv(environment)
override fun noArmor(): ExtractCert = apply { commandList.add("--no-armor") }
override fun key(keyInputStream: InputStream): Ready =
ExternalSOP.executeTransformingOperation(
Runtime.getRuntime(), commandList, envList, keyInputStream)
}

View file

@ -1,38 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.util.Properties
import sop.Ready
import sop.external.ExternalSOP
import sop.operation.GenerateKey
/** Implementation of the [GenerateKey] operation using an external SOP binary. */
class GenerateKeyExternal(binary: String, environment: Properties) : GenerateKey {
private val commandList = mutableListOf(binary, "generate-key")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var argCounter = 0
override fun noArmor(): GenerateKey = apply { commandList.add("--no-armor") }
override fun userId(userId: String): GenerateKey = apply { commandList.add(userId) }
override fun withKeyPassword(password: String): GenerateKey = apply {
commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCounter")
envList.add("KEY_PASSWORD_$argCounter=$password")
argCounter += 1
}
override fun profile(profile: String): GenerateKey = apply {
commandList.add("--profile=$profile")
}
override fun signingOnly(): GenerateKey = apply { commandList.add("--signing-only") }
override fun generate(): Ready =
ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList)
}

View file

@ -1,82 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.*
import java.util.*
import sop.ReadyWithResult
import sop.Signatures
import sop.external.ExternalSOP
import sop.external.ExternalSOP.Companion.finish
import sop.operation.InlineDetach
/** Implementation of the [InlineDetach] operation using an external SOP binary. */
class InlineDetachExternal(
binary: String,
environment: Properties,
private val tempDirProvider: ExternalSOP.TempDirProvider
) : InlineDetach {
private val commandList = mutableListOf(binary, "inline-detach")
private val envList = ExternalSOP.propertiesToEnv(environment)
override fun noArmor(): InlineDetach = apply { commandList.add("--no-armor") }
override fun message(messageInputStream: InputStream): ReadyWithResult<Signatures> {
val tempDir = tempDirProvider.provideTempDirectory()
val signaturesOut = File(tempDir, "signatures")
signaturesOut.delete()
commandList.add("--signatures-out=${signaturesOut.absolutePath}")
try {
val process =
Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray())
val processOut = process.outputStream
val processIn = process.inputStream
return object : ReadyWithResult<Signatures>() {
override fun writeTo(outputStream: OutputStream): Signatures {
val buf = ByteArray(4096)
var r: Int
while (messageInputStream.read(buf).also { r = it } > 0) {
processOut.write(buf, 0, r)
}
messageInputStream.close()
processOut.close()
while (processIn.read(buf).also { r = it } > 0) {
outputStream.write(buf, 0, r)
}
processIn.close()
outputStream.close()
finish(process)
val signaturesOutIn = FileInputStream(signaturesOut)
val signaturesBuffer = ByteArrayOutputStream()
while (signaturesOutIn.read(buf).also { r = it } > 0) {
signaturesBuffer.write(buf, 0, r)
}
signaturesOutIn.close()
signaturesOut.delete()
val sigBytes = signaturesBuffer.toByteArray()
return object : Signatures() {
@Throws(IOException::class)
override fun writeTo(outputStream: OutputStream) {
outputStream.write(sigBytes)
}
}
}
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -1,40 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.InputStream
import java.util.Properties
import sop.Ready
import sop.enums.InlineSignAs
import sop.external.ExternalSOP
import sop.operation.InlineSign
/** Implementation of the [InlineSign] operation using an external SOP binary. */
class InlineSignExternal(binary: String, environment: Properties) : InlineSign {
private val commandList = mutableListOf(binary, "inline-sign")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var argCounter = 0
override fun mode(mode: InlineSignAs): InlineSign = apply { commandList.add("--as=$mode") }
override fun data(data: InputStream): Ready =
ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data)
override fun noArmor(): InlineSign = apply { commandList.add("--no-armor") }
override fun key(key: InputStream): InlineSign = apply {
commandList.add("@ENV:KEY_$argCounter")
envList.add("KEY_$argCounter=${ExternalSOP.readString(key)}")
argCounter += 1
}
override fun withKeyPassword(password: ByteArray): InlineSign = apply {
commandList.add("--with-key-password=@ENV:WITH_KEY_PASSWORD_$argCounter")
envList.add("WITH_KEY_PASSWORD_$argCounter=${String(password)}")
argCounter += 1
}
}

View file

@ -1,91 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.*
import java.util.*
import sop.ReadyWithResult
import sop.Verification
import sop.Verification.Companion.fromString
import sop.external.ExternalSOP
import sop.external.ExternalSOP.Companion.finish
import sop.operation.InlineVerify
import sop.util.UTCUtil
/** Implementation of the [InlineVerify] operation using an external SOP binary. */
class InlineVerifyExternal(
binary: String,
environment: Properties,
private val tempDirProvider: ExternalSOP.TempDirProvider
) : InlineVerify {
private val commandList = mutableListOf(binary, "inline-verify")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var argCounter = 0
override fun data(data: InputStream): ReadyWithResult<List<Verification>> {
val tempDir = tempDirProvider.provideTempDirectory()
val verificationsOut = File(tempDir, "verifications-out")
verificationsOut.delete()
commandList.add("--verifications-out=${verificationsOut.absolutePath}")
try {
val process =
Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray())
val processOut = process.outputStream
val processIn = process.inputStream
return object : ReadyWithResult<List<Verification>>() {
override fun writeTo(outputStream: OutputStream): List<Verification> {
val buf = ByteArray(4096)
var r: Int
while (data.read(buf).also { r = it } > 0) {
processOut.write(buf, 0, r)
}
data.close()
processOut.close()
while (processIn.read(buf).also { r = it } > 0) {
outputStream.write(buf, 0, r)
}
processIn.close()
outputStream.close()
finish(process)
val verificationsOutIn = FileInputStream(verificationsOut)
val reader = BufferedReader(InputStreamReader(verificationsOutIn))
val verificationList: MutableList<Verification> = mutableListOf()
var line: String?
while (reader.readLine().also { line = it } != null) {
verificationList.add(fromString(line!!.trim()))
}
return verificationList
}
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
override fun notBefore(timestamp: Date): InlineVerify = apply {
commandList.add("--not-before=${UTCUtil.formatUTCDate(timestamp)}")
}
override fun notAfter(timestamp: Date): InlineVerify = apply {
commandList.add("--not-after=${UTCUtil.formatUTCDate(timestamp)}")
}
override fun cert(cert: InputStream): InlineVerify = apply {
commandList.add("@ENV:CERT_$argCounter")
envList.add("CERT_$argCounter=${ExternalSOP.readString(cert)}")
argCounter += 1
}
}

View file

@ -1,36 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.IOException
import java.util.Properties
import sop.Profile
import sop.external.ExternalSOP
import sop.operation.ListProfiles
/** Implementation of the [ListProfiles] operation using an external SOP binary. */
class ListProfilesExternal(binary: String, environment: Properties) : ListProfiles {
private val commandList = mutableListOf(binary, "list-profiles")
private val envList = ExternalSOP.propertiesToEnv(environment)
override fun subcommand(command: String): List<Profile> {
return try {
String(
ExternalSOP.executeProducingOperation(
Runtime.getRuntime(), commandList.plus(command), envList)
.bytes)
.let { toProfiles(it) }
} catch (e: IOException) {
throw RuntimeException(e)
}
}
companion object {
@JvmStatic
private fun toProfiles(output: String): List<Profile> =
output.split("\n").filter { it.isNotBlank() }.map { Profile.parse(it) }
}
}

View file

@ -1,31 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.InputStream
import java.util.Properties
import sop.Ready
import sop.external.ExternalSOP
import sop.operation.RevokeKey
/** Implementation of the [RevokeKey] operation using an external SOP binary. */
class RevokeKeyExternal(binary: String, environment: Properties) : RevokeKey {
private val commandList = mutableListOf(binary, "revoke-key")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var argCount = 0
override fun noArmor(): RevokeKey = apply { commandList.add("--no-armor") }
override fun withKeyPassword(password: ByteArray): RevokeKey = apply {
commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount")
envList.add("KEY_PASSWORD_$argCount=${String(password)}")
argCount += 1
}
override fun keys(keys: InputStream): Ready =
ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys)
}

View file

@ -1,102 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.IOException
import java.util.Properties
import sop.external.ExternalSOP
import sop.operation.Version
/** Implementation of the [Version] operation using an external SOP binary. */
class VersionExternal(binary: String, environment: Properties) : Version {
private val commandList = listOf(binary, "version")
private val envList = ExternalSOP.propertiesToEnv(environment)
override fun getName(): String {
val info = executeForLine(commandList)
return if (info.contains(" ")) {
info.substring(0, info.lastIndexOf(" "))
} else {
info
}
}
override fun getVersion(): String {
val info = executeForLine(commandList)
return if (info.contains(" ")) {
info.substring(info.lastIndexOf(" ") + 1)
} else {
info
}
}
override fun getBackendVersion(): String {
return executeForLines(commandList.plus("--backend"))
}
override fun getExtendedVersion(): String {
return executeForLines(commandList.plus("--extended"))
}
override fun getSopSpecRevisionNumber(): Int {
val revision = getSopSpecVersion()
val firstLine =
if (revision.contains("\n")) {
revision.substring(0, revision.indexOf("\n"))
} else {
revision
}
if (!firstLine.contains("-")) {
return -1
}
return Integer.parseInt(firstLine.substring(firstLine.lastIndexOf("-") + 1))
}
override fun isSopSpecImplementationIncomplete(): Boolean {
return getSopSpecVersion().startsWith("~")
}
override fun getSopSpecImplementationRemarks(): String? {
val revision = getSopSpecVersion()
if (revision.contains("\n")) {
revision.substring(revision.indexOf("\n")).trim().takeIf { it.isNotBlank() }
}
return null
}
override fun getSopVVersion(): String {
return executeForLines(commandList.plus("--sopv"))
}
override fun getSopSpecVersion(): String {
return executeForLines(commandList.plus("--sop-spec"))
}
private fun executeForLine(commandList: List<String>): String {
return try {
val process =
Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray())
val result = process.inputStream.bufferedReader().readLine()
ExternalSOP.finish(process)
result.trim()
} catch (e: IOException) {
throw RuntimeException(e)
}
}
private fun executeForLines(commandList: List<String>): String {
return try {
val process =
Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray())
val result = process.inputStream.bufferedReader().readLines().joinToString("\n")
ExternalSOP.finish(process)
result.trim()
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -1,5 +0,0 @@
# SPDX-FileCopyrightText: 2023 Paul Schaub <info@pgpainless.org>
#
# SPDX-License-Identifier: CC0-1.0
config.json

View file

@ -1,8 +0,0 @@
{
"backends": [
{
"name": "Sequoia-SOP",
"sop": "/usr/bin/sqop"
}
]
}

View file

@ -1,17 +0,0 @@
{
"backends": [
{
"name": "Example-SOP",
"sop": "/usr/bin/example-sop"
},
{
"name": "Awesome-SOP",
"sop": "/usr/local/bin/awesome-sop",
"env": [
{
"key": "myEnvironmentVariable", "value": "FooBar"
}
]
}
]
}

View file

@ -1,80 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external;
import com.google.gson.Gson;
import sop.SOP;
import sop.external.ExternalSOP;
import sop.testsuite.SOPInstanceFactory;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* This implementation of {@link SOPInstanceFactory} reads the JSON file at
* <pre>external-sop/src/main/resources/sop/testsuite/external/config.json</pre>
* to determine configured external test backends.
*/
public class ExternalSOPInstanceFactory extends SOPInstanceFactory {
@Override
public Map<String, SOP> provideSOPInstances() {
Map<String, SOP> backends = new HashMap<>();
TestSuite suite = readConfiguration();
if (suite != null && !suite.backends.isEmpty()) {
for (TestSubject subject : suite.backends) {
if (!new File(subject.sop).exists()) {
continue;
}
Properties env = new Properties();
if (subject.env != null) {
for (Var var : subject.env) {
env.put(var.key, var.value);
}
}
SOP sop = new ExternalSOP(subject.sop, env);
backends.put(subject.name, sop);
}
}
return backends;
}
public static TestSuite readConfiguration() {
Gson gson = new Gson();
InputStream inputStream = ExternalSOPInstanceFactory.class.getResourceAsStream("config.json");
if (inputStream == null) {
return null;
}
InputStreamReader reader = new InputStreamReader(inputStream);
return gson.fromJson(reader, TestSuite.class);
}
// JSON DTOs
public static class TestSuite {
List<TestSubject> backends;
}
public static class TestSubject {
String name;
String sop;
List<Var> env;
}
public static class Var {
String key;
String value;
}
}

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

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

257
gradlew vendored
View file

@ -1,7 +1,7 @@
#!/bin/sh
#!/usr/bin/env sh
#
# Copyright © 2015-2021 the original authors.
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -17,101 +17,67 @@
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
MAX_FD="maximum"
warn () {
echo "$*"
} >&2
}
die () {
echo
echo "$*"
echo
exit 1
} >&2
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MSYS* | MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@ -121,9 +87,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD=$JAVA_HOME/bin/java
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -132,7 +98,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
@ -140,95 +106,80 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=$( cygpath --unix "$JAVACMD" )
JAVACMD=`cygpath --unix "$JAVACMD"`
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

View file

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

View file

@ -8,7 +8,7 @@ SPDX-License-Identifier: Apache-2.0
[![javadoc](https://javadoc.io/badge2/org.pgpainless/sop-java-picocli/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/sop-java-picocli)
[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/sop-java-picocli)](https://search.maven.org/artifact/org.pgpainless/sop-java-picocli)
Implementation of the [Stateless OpenPGP Command Line Interface](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) specification.
Implementation of the [Stateless OpenPGP Command Line Interface](https://tools.ietf.org/html/draft-dkg-openpgp-stateless-cli-01) specification.
This terminal application allows generation of OpenPGP keys, extraction of public key certificates,
armoring and de-armoring of data, as well as - of course - encryption/decryption of messages and creation/verification of signatures.
@ -34,4 +34,4 @@ java -jar sop-java-picocli-XXX.jar help
If you just want to get started encrypting messages, see the module `pgpainless-cli` which initializes
`sop-java-picocli` with `pgpainless-sop`, so you can get started right away without the need to manually wire stuff up.
Enjoy!
Enjoy!

View file

@ -4,7 +4,6 @@
plugins {
id 'application'
id 'org.asciidoctor.jvm.convert' version '3.3.2'
}
dependencies {
@ -12,16 +11,18 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
// Testing Exit Codes in JUnit
// https://todd.ginsberg.com/post/testing-system-exit/
testImplementation "com.ginsberg:junit5-system-exit:$junitSysExitVersion"
// Mocking Components
testImplementation "org.mockito:mockito-core:$mockitoVersion"
// SOP
implementation(project(":sop-java"))
testImplementation(testFixtures(project(":sop-java")))
// CLI
implementation "info.picocli:picocli:$picocliVersion"
annotationProcessor "info.picocli:picocli-codegen:$picocliVersion"
// @Nonnull, @Nullable...
implementation "com.google.code.findbugs:jsr305:$jsrVersion"
@ -35,7 +36,6 @@ application {
jar {
dependsOn(":sop-java:jar")
duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
manifest {
attributes 'Main-Class': "$mainClassName"
@ -49,25 +49,3 @@ jar {
exclude "META-INF/*.RSA"
}
}
task generateManpageAsciiDoc(type: JavaExec) {
dependsOn(classes)
group = "Documentation"
description = "Generate AsciiDoc manpage"
classpath(configurations.annotationProcessor, sourceSets.main.runtimeClasspath)
systemProperty("user.language", "en")
main 'picocli.codegen.docgen.manpage.ManPageGenerator'
args mainClassName, "--outdir=${project.buildDir}/generated-picocli-docs", "-v" //, "--template-dir=src/docs/mantemplates"
}
apply plugin: 'org.asciidoctor.jvm.convert'
asciidoctor {
attributes 'reproducible': ''
dependsOn(generateManpageAsciiDoc)
sourceDir = file("${project.buildDir}/generated-picocli-docs")
outputDir = file("${project.buildDir}/docs")
logDocuments = true
outputOptions {
backends = ['manpage', 'html5']
}
}

View file

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli;
import java.util.Date;
import sop.util.UTCUtil;
public class DateParser {
public static final Date BEGINNING_OF_TIME = new Date(0);
public static final Date END_OF_TIME = new Date(8640000000000000L);
public static Date parseNotAfter(String notAfter) {
Date date = notAfter.equals("now") ? new Date() : notAfter.equals("-") ? END_OF_TIME : UTCUtil.parseUTCDate(notAfter);
if (date == null) {
Print.errln("Invalid date string supplied as value of --not-after.");
System.exit(1);
}
return date;
}
public static Date parseNotBefore(String notBefore) {
Date date = notBefore.equals("now") ? new Date() : notBefore.equals("-") ? BEGINNING_OF_TIME : UTCUtil.parseUTCDate(notBefore);
if (date == null) {
Print.errln("Invalid date string supplied as value of --not-before.");
System.exit(1);
}
return date;
}
}

View file

@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.IOException;
import sop.exception.SOPGPException;
import sop.util.UTF8Util;
public class FileUtil {
private static final String ERROR_AMBIGUOUS = "File name '%s' is ambiguous. File with the same name exists on the filesystem.";
private static final String ERROR_ENV_FOUND = "Environment variable '%s' not set.";
private static final String ERROR_OUTPUT_EXISTS = "Output file '%s' already exists.";
private static final String ERROR_INPUT_NOT_EXIST = "File '%s' does not exist.";
private static final String ERROR_CANNOT_CREATE_FILE = "Output file '%s' cannot be created: %s";
public static final String PRFX_ENV = "@ENV:";
public static final String PRFX_FD = "@FD:";
private static EnvironmentVariableResolver envResolver = System::getenv;
public static void setEnvironmentVariableResolver(EnvironmentVariableResolver envResolver) {
if (envResolver == null) {
throw new NullPointerException("Variable envResolver cannot be null.");
}
FileUtil.envResolver = envResolver;
}
public interface EnvironmentVariableResolver {
/**
* Resolve the value of the given environment variable.
* Return null if the variable is not present.
*
* @param name name of the variable
* @return variable value or null
*/
String resolveEnvironmentVariable(String name);
}
public static File getFile(String fileName) {
if (fileName == null) {
throw new NullPointerException("File name cannot be null.");
}
if (fileName.startsWith(PRFX_ENV)) {
if (new File(fileName).exists()) {
throw new SOPGPException.AmbiguousInput(String.format(ERROR_AMBIGUOUS, fileName));
}
String envName = fileName.substring(PRFX_ENV.length());
String envValue = envResolver.resolveEnvironmentVariable(envName);
if (envValue == null) {
throw new IllegalArgumentException(String.format(ERROR_ENV_FOUND, envName));
}
return new File(envValue);
} else if (fileName.startsWith(PRFX_FD)) {
if (new File(fileName).exists()) {
throw new SOPGPException.AmbiguousInput(String.format(ERROR_AMBIGUOUS, fileName));
}
throw new IllegalArgumentException("File descriptors not supported.");
}
return new File(fileName);
}
public static FileInputStream getFileInputStream(String fileName) {
File file = getFile(fileName);
try {
FileInputStream inputStream = new FileInputStream(file);
return inputStream;
} catch (FileNotFoundException e) {
throw new SOPGPException.MissingInput(String.format(ERROR_INPUT_NOT_EXIST, fileName), e);
}
}
public static File createNewFileOrThrow(File file) throws IOException {
if (file == null) {
throw new NullPointerException("File cannot be null.");
}
try {
if (!file.createNewFile()) {
throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_EXISTS, file.getAbsolutePath()));
}
} catch (IOException e) {
throw new IOException(String.format(ERROR_CANNOT_CREATE_FILE, file.getAbsolutePath(), e.getMessage()));
}
return file;
}
public static String stringFromInputStream(InputStream inputStream) throws IOException {
try {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
byte[] buf = new byte[4096]; int read;
while ((read = inputStream.read(buf)) != -1) {
byteOut.write(buf, 0, read);
}
// TODO: For decrypt operations we MUST accept non-UTF8 passwords
return UTF8Util.decodeUTF8(byteOut.toByteArray());
} finally {
inputStream.close();
}
}
}

View file

@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli;
public class Print {
public static void errln(String string) {
// CHECKSTYLE:OFF
System.err.println(string);
// CHECKSTYLE:ON
}
public static void trace(Throwable e) {
// CHECKSTYLE:OFF
e.printStackTrace();
// CHECKSTYLE:ON
}
public static void outln(String string) {
// CHECKSTYLE:OFF
System.out.println(string);
// CHECKSTYLE:ON
}
}

View file

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli;
import picocli.CommandLine;
import sop.exception.SOPGPException;
public class SOPExceptionExitCodeMapper implements CommandLine.IExitCodeExceptionMapper {
@Override
public int getExitCode(Throwable exception) {
if (exception instanceof SOPGPException) {
return ((SOPGPException) exception).getExitCode();
}
if (exception instanceof CommandLine.UnmatchedArgumentException) {
CommandLine.UnmatchedArgumentException ex = (CommandLine.UnmatchedArgumentException) exception;
// Unmatched option of subcommand (eg. `generate-key -k`)
if (ex.isUnknownOption()) {
return SOPGPException.UnsupportedOption.EXIT_CODE;
}
// Unmatched subcommand
return SOPGPException.UnsupportedSubcommand.EXIT_CODE;
}
// Invalid option (eg. `--label Invalid`)
if (exception instanceof CommandLine.ParameterException) {
return SOPGPException.UnsupportedOption.EXIT_CODE;
}
// Others, like IOException etc.
return 1;
}
}

View file

@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli;
import picocli.CommandLine;
public class SOPExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler {
@Override
public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) {
int exitCode = commandLine.getExitCodeExceptionMapper() != null ?
commandLine.getExitCodeExceptionMapper().getExitCode(ex) :
commandLine.getCommandSpec().exitCodeOnExecutionException();
CommandLine.Help.ColorScheme colorScheme = commandLine.getColorScheme();
// CHECKSTYLE:OFF
if (ex.getMessage() != null) {
commandLine.getErr().println(colorScheme.errorText(ex.getMessage()));
}
ex.printStackTrace(commandLine.getErr());
// CHECKSTYLE:ON
return exitCode;
}
}

View file

@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli;
import picocli.AutoComplete;
import picocli.CommandLine;
import sop.SOP;
import sop.cli.picocli.commands.ArmorCmd;
import sop.cli.picocli.commands.DearmorCmd;
import sop.cli.picocli.commands.DecryptCmd;
import sop.cli.picocli.commands.DetachInbandSignatureAndMessageCmd;
import sop.cli.picocli.commands.EncryptCmd;
import sop.cli.picocli.commands.ExtractCertCmd;
import sop.cli.picocli.commands.GenerateKeyCmd;
import sop.cli.picocli.commands.SignCmd;
import sop.cli.picocli.commands.VerifyCmd;
import sop.cli.picocli.commands.VersionCmd;
@CommandLine.Command(
name = "sop",
description = "Stateless OpenPGP Protocol",
exitCodeOnInvalidInput = 69,
subcommands = {
CommandLine.HelpCommand.class,
ArmorCmd.class,
DearmorCmd.class,
DecryptCmd.class,
DetachInbandSignatureAndMessageCmd.class,
EncryptCmd.class,
ExtractCertCmd.class,
GenerateKeyCmd.class,
SignCmd.class,
VerifyCmd.class,
VersionCmd.class,
AutoComplete.GenerateCompletion.class
},
exitCodeListHeading = "Exit Codes:%n",
exitCodeList = {
" 0:Successful program execution",
" 1:Generic program error",
" 3:Verification requested but no verifiable signature found",
"13:Unsupported asymmetric algorithm",
"17:Certificate is not encryption capable",
"19:Usage error: Missing argument",
"23:Incomplete verification instructions",
"29:Unable to decrypt",
"31:Password is not human-readable",
"37:Unsupported Option",
"41:Invalid data or data of wrong type encountered",
"53:Non-text input received where text was expected",
"59:Output file already exists",
"61:Input file does not exist",
"67:Key is password protected",
"69:Unsupported subcommand",
"71:Unsupported special prefix (e.g. \"@env/@fd\") of indirect parameter",
"73:Ambiguous input (a filename matching the designator already exists)",
"79:Key is not signing capable"
}
)
public class SopCLI {
// Singleton
static SOP SOP_INSTANCE;
public static String EXECUTABLE_NAME = "sop";
public static void main(String[] args) {
int exitCode = execute(args);
if (exitCode != 0) {
System.exit(exitCode);
}
}
public static int execute(String[] args) {
CommandLine cmd = new CommandLine(SopCLI.class);
// Hide generate-completion command
CommandLine gen = cmd.getSubcommands().get("generate-completion");
gen.getCommandSpec().usageMessage().hidden(true);
cmd.setCommandName(EXECUTABLE_NAME)
.setExecutionExceptionHandler(new SOPExecutionExceptionHandler())
.setExitCodeExceptionMapper(new SOPExceptionExitCodeMapper())
.setCaseInsensitiveEnumValuesAllowed(true);
return cmd.execute(args);
}
public static SOP getSop() {
if (SOP_INSTANCE == null) {
throw new IllegalStateException("No SOP backend set.");
}
return SOP_INSTANCE;
}
public static void setSopInstance(SOP instance) {
SOP_INSTANCE = instance;
}
}

View file

@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import java.io.IOException;
import picocli.CommandLine;
import sop.Ready;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.enums.ArmorLabel;
import sop.exception.SOPGPException;
import sop.operation.Armor;
@CommandLine.Command(name = "armor",
description = "Add ASCII Armor to standard input",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class ArmorCmd implements Runnable {
@CommandLine.Option(names = {"--label"}, description = "Label to be used in the header and tail of the armoring.", paramLabel = "{auto|sig|key|cert|message}")
ArmorLabel label;
@Override
public void run() {
Armor armor = SopCLI.getSop().armor();
if (armor == null) {
throw new SOPGPException.UnsupportedSubcommand("Command 'armor' not implemented.");
}
if (label != null) {
try {
armor.label(label);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Armor labels not supported.");
System.exit(unsupportedOption.getExitCode());
}
}
try {
Ready ready = armor.data(System.in);
ready.writeTo(System.out);
} catch (SOPGPException.BadData badData) {
Print.errln("Bad data.");
Print.trace(badData);
System.exit(badData.getExitCode());
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
}
}
}

View file

@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import java.io.IOException;
import picocli.CommandLine;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Dearmor;
@CommandLine.Command(name = "dearmor",
description = "Remove ASCII Armor from standard input",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class DearmorCmd implements Runnable {
@Override
public void run() {
Dearmor dearmor = SopCLI.getSop().dearmor();
if (dearmor == null) {
throw new SOPGPException.UnsupportedSubcommand("Command 'dearmor' not implemented.");
}
try {
SopCLI.getSop()
.dearmor()
.data(System.in)
.writeTo(System.out);
} catch (SOPGPException.BadData e) {
Print.errln("Bad data.");
Print.trace(e);
System.exit(e.getExitCode());
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
}
}
}

View file

@ -0,0 +1,249 @@
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Pattern;
import picocli.CommandLine;
import sop.DecryptionResult;
import sop.ReadyWithResult;
import sop.SessionKey;
import sop.Verification;
import sop.cli.picocli.DateParser;
import sop.cli.picocli.FileUtil;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Decrypt;
import sop.util.HexUtil;
@CommandLine.Command(name = "decrypt",
description = "Decrypt a message from standard input",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class DecryptCmd implements Runnable {
private static final String SESSION_KEY_OUT = "--session-key-out";
private static final String VERIFY_OUT = "--verify-out";
private static final String ERROR_UNSUPPORTED_OPTION = "Option '%s' is not supported.";
private static final String ERROR_FILE_NOT_EXIST = "File '%s' does not exist.";
private static final String ERROR_OUTPUT_OF_OPTION_EXISTS = "Target %s of option %s already exists.";
@CommandLine.Option(
names = {SESSION_KEY_OUT},
description = "Can be used to learn the session key on successful decryption",
paramLabel = "SESSIONKEY")
File sessionKeyOut;
@CommandLine.Option(
names = {"--with-session-key"},
description = "Provide a session key file. Enables decryption of the \"CIPHERTEXT\" using the session key directly against the \"SEIPD\" packet",
paramLabel = "SESSIONKEY")
List<String> withSessionKey = new ArrayList<>();
@CommandLine.Option(
names = {"--with-password"},
description = "Provide a password file. Enables decryption based on any \"SKESK\" packets in the \"CIPHERTEXT\"",
paramLabel = "PASSWORD")
List<String> withPassword = new ArrayList<>();
@CommandLine.Option(names = {VERIFY_OUT},
description = "Produces signature verification status to the designated file",
paramLabel = "VERIFICATIONS")
File verifyOut;
@CommandLine.Option(names = {"--verify-with"},
description = "Certificates whose signatures would be acceptable for signatures over this message",
paramLabel = "CERT")
List<File> certs = new ArrayList<>();
@CommandLine.Option(names = {"--not-before"},
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
"Reject signatures with a creation date not in range.\n" +
"Defaults to beginning of time (\"-\").",
paramLabel = "DATE")
String notBefore = "-";
@CommandLine.Option(names = {"--not-after"},
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
"Reject signatures with a creation date not in range.\n" +
"Defaults to current system time (\"now\").\n" +
"Accepts special value \"-\" for end of time.",
paramLabel = "DATE")
String notAfter = "now";
@CommandLine.Parameters(index = "0..*",
description = "Secret keys to attempt decryption with",
paramLabel = "KEY")
List<File> keys = new ArrayList<>();
@Override
public void run() {
throwIfOutputExists(verifyOut, VERIFY_OUT);
throwIfOutputExists(sessionKeyOut, SESSION_KEY_OUT);
Decrypt decrypt = SopCLI.getSop().decrypt();
if (decrypt == null) {
throw new SOPGPException.UnsupportedSubcommand("Command 'decrypt' not implemented.");
}
setNotAfter(notAfter, decrypt);
setNotBefore(notBefore, decrypt);
setWithPasswords(withPassword, decrypt);
setWithSessionKeys(withSessionKey, decrypt);
setVerifyWith(certs, decrypt);
setDecryptWith(keys, decrypt);
if (verifyOut != null && certs.isEmpty()) {
String errorMessage = "Option %s is requested, but no option %s was provided.";
throw new SOPGPException.IncompleteVerification(String.format(errorMessage, VERIFY_OUT, "--verify-with"));
}
try {
ReadyWithResult<DecryptionResult> ready = decrypt.ciphertext(System.in);
DecryptionResult result = ready.writeTo(System.out);
writeSessionKeyOut(result);
writeVerifyOut(result);
} catch (SOPGPException.BadData badData) {
throw new SOPGPException.BadData("No valid OpenPGP message found on Standard Input.", badData);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
}
}
private void throwIfOutputExists(File outputFile, String optionName) {
if (outputFile == null) {
return;
}
if (outputFile.exists()) {
throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_OF_OPTION_EXISTS, outputFile.getAbsolutePath(), optionName));
}
}
private void writeVerifyOut(DecryptionResult result) throws IOException {
if (verifyOut != null) {
FileUtil.createNewFileOrThrow(verifyOut);
try (FileOutputStream outputStream = new FileOutputStream(verifyOut)) {
PrintWriter writer = new PrintWriter(outputStream);
for (Verification verification : result.getVerifications()) {
// CHECKSTYLE:OFF
writer.println(verification.toString());
// CHECKSTYLE:ON
}
writer.flush();
}
}
}
private void writeSessionKeyOut(DecryptionResult result) throws IOException {
if (sessionKeyOut != null) {
FileUtil.createNewFileOrThrow(sessionKeyOut);
try (FileOutputStream outputStream = new FileOutputStream(sessionKeyOut)) {
if (!result.getSessionKey().isPresent()) {
throw new SOPGPException.UnsupportedOption("Session key not extracted. Possibly the feature --session-key-out is not supported.");
} else {
SessionKey sessionKey = result.getSessionKey().get();
outputStream.write(sessionKey.getAlgorithm());
outputStream.write(sessionKey.getKey());
}
}
}
}
private void setDecryptWith(List<File> keys, Decrypt decrypt) {
for (File key : keys) {
try (FileInputStream keyIn = new FileInputStream(key)) {
decrypt.withKey(keyIn);
} catch (SOPGPException.KeyIsProtected keyIsProtected) {
throw new SOPGPException.KeyIsProtected("Key in file " + key.getAbsolutePath() + " is password protected.", keyIsProtected);
} catch (SOPGPException.BadData badData) {
throw new SOPGPException.BadData("File " + key.getAbsolutePath() + " does not contain a private key.", badData);
} catch (FileNotFoundException e) {
throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, key.getAbsolutePath()), e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private void setVerifyWith(List<File> certs, Decrypt decrypt) {
for (File cert : certs) {
try (FileInputStream certIn = new FileInputStream(cert)) {
decrypt.verifyWithCert(certIn);
} catch (FileNotFoundException e) {
throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, cert.getAbsolutePath()), e);
} catch (SOPGPException.BadData badData) {
throw new SOPGPException.BadData("File " + cert.getAbsolutePath() + " does not contain a valid certificate.", badData);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
}
}
}
private void setWithSessionKeys(List<String> withSessionKey, Decrypt decrypt) {
Pattern sessionKeyPattern = Pattern.compile("^\\d+:[0-9A-F]+$");
for (String sessionKeyFile : withSessionKey) {
String sessionKey;
try {
sessionKey = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(sessionKeyFile));
} catch (IOException e) {
throw new RuntimeException(e);
}
if (!sessionKeyPattern.matcher(sessionKey).matches()) {
throw new IllegalArgumentException("Session keys are expected in the format 'ALGONUM:HEXKEY'.");
}
String[] split = sessionKey.split(":");
byte algorithm = (byte) Integer.parseInt(split[0]);
byte[] key = HexUtil.hexToBytes(split[1]);
try {
decrypt.withSessionKey(new SessionKey(algorithm, key));
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-session-key"), unsupportedOption);
}
}
}
private void setWithPasswords(List<String> withPassword, Decrypt decrypt) {
for (String passwordFile : withPassword) {
try {
String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(passwordFile));
decrypt.withPassword(password);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-password"), unsupportedOption);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private void setNotAfter(String notAfter, Decrypt decrypt) {
Date notAfterDate = DateParser.parseNotAfter(notAfter);
try {
decrypt.verifyNotAfter(notAfterDate);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-after"), unsupportedOption);
}
}
private void setNotBefore(String notBefore, Decrypt decrypt) {
Date notBeforeDate = DateParser.parseNotBefore(notBefore);
try {
decrypt.verifyNotBefore(notBeforeDate);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-before"), unsupportedOption);
}
}
}

View file

@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import picocli.CommandLine;
import sop.Signatures;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.DetachInbandSignatureAndMessage;
@CommandLine.Command(name = "detach-inband-signature-and-message",
description = "Split a clearsigned message",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class DetachInbandSignatureAndMessageCmd implements Runnable {
@CommandLine.Option(
names = {"--signatures-out"},
description = "Destination to which a detached signatures block will be written",
paramLabel = "SIGNATURES")
File signaturesOut;
@CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output",
negatable = true)
boolean armor = true;
@Override
public void run() {
DetachInbandSignatureAndMessage detach = SopCLI.getSop().detachInbandSignatureAndMessage();
if (detach == null) {
throw new SOPGPException.UnsupportedSubcommand("Command 'detach-inband-signature-and-message' not implemented.");
}
if (signaturesOut == null) {
throw new SOPGPException.MissingArg("--signatures-out is required.");
}
if (!armor) {
detach.noArmor();
}
try {
Signatures signatures = detach
.message(System.in).writeTo(System.out);
if (!signaturesOut.createNewFile()) {
throw new SOPGPException.OutputExists("Destination of --signatures-out already exists.");
}
signatures.writeTo(new FileOutputStream(signaturesOut));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import picocli.CommandLine;
import sop.Ready;
import sop.cli.picocli.FileUtil;
import sop.cli.picocli.SopCLI;
import sop.enums.EncryptAs;
import sop.exception.SOPGPException;
import sop.operation.Encrypt;
@CommandLine.Command(name = "encrypt",
description = "Encrypt a message from standard input",
exitCodeOnInvalidInput = 37)
public class EncryptCmd implements Runnable {
@CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output",
negatable = true)
boolean armor = true;
@CommandLine.Option(names = {"--as"},
description = "Type of the input data. Defaults to 'binary'",
paramLabel = "{binary|text|mime}")
EncryptAs type;
@CommandLine.Option(names = "--with-password",
description = "Encrypt the message with a password provided by the given password file",
paramLabel = "PASSWORD")
List<String> withPassword = new ArrayList<>();
@CommandLine.Option(names = "--sign-with",
description = "Sign the output with a private key",
paramLabel = "KEY")
List<File> signWith = new ArrayList<>();
@CommandLine.Parameters(description = "Certificates the message gets encrypted to",
index = "0..*",
paramLabel = "CERTS")
List<File> certs = new ArrayList<>();
@Override
public void run() {
Encrypt encrypt = SopCLI.getSop().encrypt();
if (encrypt == null) {
throw new SOPGPException.UnsupportedSubcommand("Command 'encrypt' not implemented.");
}
if (type != null) {
try {
encrypt.mode(type);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption("Unsupported option '--as'.", unsupportedOption);
}
}
if (withPassword.isEmpty() && certs.isEmpty()) {
throw new SOPGPException.MissingArg("At least one password file or cert file required for encryption.");
}
for (String passwordFileName : withPassword) {
try {
String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(passwordFileName));
encrypt.withPassword(password);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
throw new SOPGPException.UnsupportedOption("Unsupported option '--with-password'.", unsupportedOption);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
for (File keyFile : signWith) {
try (FileInputStream keyIn = new FileInputStream(keyFile)) {
encrypt.signWith(keyIn);
} catch (FileNotFoundException e) {
throw new SOPGPException.MissingInput("Key file " + keyFile.getAbsolutePath() + " not found.", e);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (SOPGPException.KeyIsProtected keyIsProtected) {
throw new SOPGPException.KeyIsProtected("Key from " + keyFile.getAbsolutePath() + " is password protected.", keyIsProtected);
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
throw new SOPGPException.UnsupportedAsymmetricAlgo("Key from " + keyFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo);
} catch (SOPGPException.KeyCannotSign keyCannotSign) {
throw new SOPGPException.KeyCannotSign("Key from " + keyFile.getAbsolutePath() + " cannot sign.", keyCannotSign);
} catch (SOPGPException.BadData badData) {
throw new SOPGPException.BadData("Key file " + keyFile.getAbsolutePath() + " does not contain a valid OpenPGP private key.", badData);
}
}
for (File certFile : certs) {
try (FileInputStream certIn = new FileInputStream(certFile)) {
encrypt.withCert(certIn);
} catch (FileNotFoundException e) {
throw new SOPGPException.MissingInput("Certificate file " + certFile.getAbsolutePath() + " not found.", e);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
throw new SOPGPException.UnsupportedAsymmetricAlgo("Certificate from " + certFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo);
} catch (SOPGPException.CertCannotEncrypt certCannotEncrypt) {
throw new SOPGPException.CertCannotEncrypt("Certificate from " + certFile.getAbsolutePath() + " is not capable of encryption.", certCannotEncrypt);
} catch (SOPGPException.BadData badData) {
throw new SOPGPException.BadData("Certificate file " + certFile.getAbsolutePath() + " does not contain a valid OpenPGP certificate.", badData);
}
}
if (!armor) {
encrypt.noArmor();
}
try {
Ready ready = encrypt.plaintext(System.in);
ready.writeTo(System.out);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import java.io.IOException;
import picocli.CommandLine;
import sop.Ready;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.ExtractCert;
@CommandLine.Command(name = "extract-cert",
description = "Extract a public key certificate from a secret key from standard input",
exitCodeOnInvalidInput = 37)
public class ExtractCertCmd implements Runnable {
@CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output",
negatable = true)
boolean armor = true;
@Override
public void run() {
ExtractCert extractCert = SopCLI.getSop().extractCert();
if (extractCert == null) {
throw new SOPGPException.UnsupportedSubcommand("Command 'extract-cert' not implemented.");
}
if (!armor) {
extractCert.noArmor();
}
try {
Ready ready = extractCert.key(System.in);
ready.writeTo(System.out);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (SOPGPException.BadData badData) {
throw new SOPGPException.BadData("Standard Input does not contain valid OpenPGP private key material.", badData);
}
}
}

View file

@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import picocli.CommandLine;
import sop.Ready;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.GenerateKey;
@CommandLine.Command(name = "generate-key",
description = "Generate a secret key",
exitCodeOnInvalidInput = 37)
public class GenerateKeyCmd implements Runnable {
@CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output",
negatable = true)
boolean armor = true;
@CommandLine.Parameters(description = "User-ID, eg. \"Alice <alice@example.com>\"")
List<String> userId = new ArrayList<>();
@Override
public void run() {
GenerateKey generateKey = SopCLI.getSop().generateKey();
if (generateKey == null) {
throw new SOPGPException.UnsupportedSubcommand("Command 'generate-key' not implemented.");
}
for (String userId : userId) {
generateKey.userId(userId);
}
if (!armor) {
generateKey.noArmor();
}
try {
Ready ready = generateKey.generate();
ready.writeTo(System.out);
} catch (SOPGPException.MissingArg missingArg) {
Print.errln("Missing argument.");
Print.trace(missingArg);
System.exit(missingArg.getExitCode());
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
Print.errln("Unsupported asymmetric algorithm.");
Print.trace(unsupportedAsymmetricAlgo);
System.exit(unsupportedAsymmetricAlgo.getExitCode());
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
}
}
}

View file

@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import picocli.CommandLine;
import sop.MicAlg;
import sop.ReadyWithResult;
import sop.SigningResult;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.enums.SignAs;
import sop.exception.SOPGPException;
import sop.operation.Sign;
@CommandLine.Command(name = "sign",
description = "Create a detached signature on the data from standard input",
exitCodeOnInvalidInput = 37)
public class SignCmd implements Runnable {
@CommandLine.Option(names = "--no-armor",
description = "ASCII armor the output",
negatable = true)
boolean armor = true;
@CommandLine.Option(names = "--as", description = "Defaults to 'binary'. If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53.",
paramLabel = "{binary|text}")
SignAs type;
@CommandLine.Parameters(description = "Secret keys used for signing",
paramLabel = "KEYS")
List<File> secretKeyFile = new ArrayList<>();
@CommandLine.Option(names = "--micalg-out", description = "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)",
paramLabel = "MICALG")
File micAlgOut;
@Override
public void run() {
Sign sign = SopCLI.getSop().sign();
if (sign == null) {
throw new SOPGPException.UnsupportedSubcommand("Command 'sign' not implemented.");
}
if (type != null) {
try {
sign.mode(type);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--as'");
Print.trace(unsupportedOption);
System.exit(unsupportedOption.getExitCode());
}
}
if (micAlgOut != null && micAlgOut.exists()) {
throw new SOPGPException.OutputExists(String.format("Target %s of option %s already exists.", micAlgOut.getAbsolutePath(), "--micalg-out"));
}
if (secretKeyFile.isEmpty()) {
Print.errln("Missing required parameter 'KEYS'.");
System.exit(19);
}
for (File keyFile : secretKeyFile) {
try (FileInputStream keyIn = new FileInputStream(keyFile)) {
sign.key(keyIn);
} catch (FileNotFoundException e) {
Print.errln("File " + keyFile.getAbsolutePath() + " does not exist.");
Print.trace(e);
System.exit(1);
} catch (IOException e) {
Print.errln("Cannot access file " + keyFile.getAbsolutePath());
Print.trace(e);
System.exit(1);
} catch (SOPGPException.KeyIsProtected e) {
Print.errln("Key " + keyFile.getName() + " is password protected.");
Print.trace(e);
System.exit(1);
} catch (SOPGPException.BadData badData) {
Print.errln("Bad data in key file " + keyFile.getAbsolutePath() + ":");
Print.trace(badData);
System.exit(badData.getExitCode());
}
}
if (!armor) {
sign.noArmor();
}
try {
ReadyWithResult<SigningResult> ready = sign.data(System.in);
SigningResult result = ready.writeTo(System.out);
MicAlg micAlg = result.getMicAlg();
if (micAlgOut != null) {
// Write micalg out
micAlgOut.createNewFile();
FileOutputStream micAlgOutStream = new FileOutputStream(micAlgOut);
micAlg.writeTo(micAlgOutStream);
micAlgOutStream.close();
}
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
} catch (SOPGPException.ExpectedText expectedText) {
Print.errln("Expected text input, but got binary data.");
Print.trace(expectedText);
System.exit(expectedText.getExitCode());
}
}
}

View file

@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import picocli.CommandLine;
import sop.Verification;
import sop.cli.picocli.DateParser;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Verify;
@CommandLine.Command(name = "verify",
description = "Verify a detached signature over the data from standard input",
exitCodeOnInvalidInput = 37)
public class VerifyCmd implements Runnable {
@CommandLine.Parameters(index = "0",
description = "Detached signature",
paramLabel = "SIGNATURE")
File signature;
@CommandLine.Parameters(index = "1..*",
arity = "1..*",
description = "Public key certificates",
paramLabel = "CERT")
List<File> certificates = new ArrayList<>();
@CommandLine.Option(names = {"--not-before"},
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
"Reject signatures with a creation date not in range.\n" +
"Defaults to beginning of time (\"-\").",
paramLabel = "DATE")
String notBefore = "-";
@CommandLine.Option(names = {"--not-after"},
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
"Reject signatures with a creation date not in range.\n" +
"Defaults to current system time (\"now\").\n" +
"Accepts special value \"-\" for end of time.",
paramLabel = "DATE")
String notAfter = "now";
@Override
public void run() {
Verify verify = SopCLI.getSop().verify();
if (verify == null) {
throw new SOPGPException.UnsupportedSubcommand("Command 'verify' not implemented.");
}
if (notAfter != null) {
try {
verify.notAfter(DateParser.parseNotAfter(notAfter));
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--not-after'.");
Print.trace(unsupportedOption);
System.exit(unsupportedOption.getExitCode());
}
}
if (notBefore != null) {
try {
verify.notBefore(DateParser.parseNotBefore(notBefore));
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Unsupported option '--not-before'.");
Print.trace(unsupportedOption);
System.exit(unsupportedOption.getExitCode());
}
}
for (File certFile : certificates) {
try (FileInputStream certIn = new FileInputStream(certFile)) {
verify.cert(certIn);
} catch (FileNotFoundException fileNotFoundException) {
Print.errln("Certificate file " + certFile.getAbsolutePath() + " not found.");
Print.trace(fileNotFoundException);
System.exit(1);
} catch (IOException ioException) {
Print.errln("IO Error.");
Print.trace(ioException);
System.exit(1);
} catch (SOPGPException.BadData badData) {
Print.errln("Certificate file " + certFile.getAbsolutePath() + " appears to not contain a valid OpenPGP certificate.");
Print.trace(badData);
System.exit(badData.getExitCode());
}
}
if (signature != null) {
try (FileInputStream sigIn = new FileInputStream(signature)) {
verify.signatures(sigIn);
} catch (FileNotFoundException e) {
Print.errln("Signature file " + signature.getAbsolutePath() + " does not exist.");
Print.trace(e);
System.exit(1);
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
System.exit(1);
} catch (SOPGPException.BadData badData) {
Print.errln("File " + signature.getAbsolutePath() + " does not contain a valid OpenPGP signature.");
Print.trace(badData);
System.exit(badData.getExitCode());
}
}
List<Verification> verifications = null;
try {
verifications = verify.data(System.in);
} catch (SOPGPException.NoSignature e) {
Print.errln("No verifiable signature found.");
Print.trace(e);
System.exit(e.getExitCode());
} catch (IOException ioException) {
Print.errln("IO Error.");
Print.trace(ioException);
System.exit(1);
} catch (SOPGPException.BadData badData) {
Print.errln("Standard Input appears not to contain a valid OpenPGP message.");
Print.trace(badData);
System.exit(badData.getExitCode());
}
for (Verification verification : verifications) {
Print.outln(verification.toString());
}
}
}

View file

@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Version;
@CommandLine.Command(name = "version", description = "Display version information about the tool",
exitCodeOnInvalidInput = 37)
public class VersionCmd implements Runnable {
@CommandLine.ArgGroup()
Exclusive exclusive;
static class Exclusive {
@CommandLine.Option(names = "--extended", description = "Print an extended version string.")
boolean extended;
@CommandLine.Option(names = "--backend", description = "Print information about the cryptographic backend.")
boolean backend;
}
@Override
public void run() {
Version version = SopCLI.getSop().version();
if (version == null) {
throw new SOPGPException.UnsupportedSubcommand("Command 'version' not implemented.");
}
if (exclusive == null) {
Print.outln(version.getName() + " " + version.getVersion());
return;
}
if (exclusive.extended) {
Print.outln(version.getExtendedVersion());
return;
}
if (exclusive.backend) {
Print.outln(version.getBackendVersion());
return;
}
}
}

View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Subcommands of the PGPainless SOP.
*/
package sop.cli.picocli.commands;

View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Implementation of the Stateless OpenPGP Command Line Interface using Picocli.
*/
package sop.cli.picocli;

View file

@ -1,31 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli
import picocli.CommandLine.*
import sop.exception.SOPGPException
class SOPExceptionExitCodeMapper : IExitCodeExceptionMapper {
override fun getExitCode(exception: Throwable): Int =
if (exception is SOPGPException) {
// SOPGPExceptions have well-defined exit code
exception.getExitCode()
} else if (exception is UnmatchedArgumentException) {
if (exception.isUnknownOption) {
// Unmatched option of subcommand (e.g. `generate-key --unknown`)
SOPGPException.UnsupportedOption.EXIT_CODE
} else {
// Unmatched subcommand
SOPGPException.UnsupportedSubcommand.EXIT_CODE
}
} else if (exception is ParameterException) {
// Invalid option (e.g. `--as invalid`)
SOPGPException.UnsupportedOption.EXIT_CODE
} else {
// Others, like IOException etc.
1
}
}

View file

@ -1,34 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli
import picocli.CommandLine
import picocli.CommandLine.IExecutionExceptionHandler
class SOPExecutionExceptionHandler : IExecutionExceptionHandler {
override fun handleExecutionException(
ex: Exception,
commandLine: CommandLine,
parseResult: CommandLine.ParseResult
): Int {
val exitCode =
if (commandLine.exitCodeExceptionMapper != null)
commandLine.exitCodeExceptionMapper.getExitCode(ex)
else commandLine.commandSpec.exitCodeOnExecutionException()
val colorScheme = commandLine.colorScheme
if (ex.message != null) {
commandLine.getErr().println(colorScheme.errorText(ex.message))
} else {
commandLine.getErr().println(ex.javaClass.getName())
}
if (SopCLI.stacktrace) {
ex.printStackTrace(commandLine.getErr())
}
return exitCode
}
}

View file

@ -1,113 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli
import java.util.*
import kotlin.system.exitProcess
import picocli.AutoComplete.GenerateCompletion
import picocli.CommandLine
import picocli.CommandLine.*
import sop.SOP
import sop.cli.picocli.commands.*
import sop.exception.SOPGPException
@Command(
name = "sop",
resourceBundle = "msg_sop",
exitCodeOnInvalidInput = SOPGPException.UnsupportedSubcommand.EXIT_CODE,
subcommands =
[
// Meta subcommands
VersionCmd::class,
ListProfilesCmd::class,
// Key and certificate management
GenerateKeyCmd::class,
ChangeKeyPasswordCmd::class,
RevokeKeyCmd::class,
ExtractCertCmd::class,
// Messaging subcommands
SignCmd::class,
VerifyCmd::class,
EncryptCmd::class,
DecryptCmd::class,
InlineDetachCmd::class,
InlineSignCmd::class,
InlineVerifyCmd::class,
// Transport
ArmorCmd::class,
DearmorCmd::class,
// misc
HelpCommand::class,
GenerateCompletion::class])
class SopCLI {
companion object {
@JvmStatic private var sopInstance: SOP? = null
@JvmStatic
fun getSop(): SOP =
checkNotNull(sopInstance) { cliMsg.getString("sop.error.runtime.no_backend_set") }
@JvmStatic
fun setSopInstance(sop: SOP?) {
sopInstance = sop
}
@JvmField var cliMsg: ResourceBundle = ResourceBundle.getBundle("msg_sop")
@JvmField var EXECUTABLE_NAME = "sop"
@JvmField
@Option(names = ["--stacktrace"], scope = CommandLine.ScopeType.INHERIT)
var stacktrace = false
@JvmStatic
fun main(vararg args: String) {
val exitCode = execute(*args)
if (exitCode != 0) {
exitProcess(exitCode)
}
}
@JvmStatic
fun execute(vararg args: String): Int {
// Set locale
CommandLine(InitLocale()).parseArgs(*args)
// Re-set bundle with updated locale
cliMsg = ResourceBundle.getBundle("msg_sop")
return CommandLine(SopCLI::class.java)
.apply {
// explicitly set help command resource bundle
subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help"))
// Hide generate-completion command
subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true)
// overwrite executable name
commandName = EXECUTABLE_NAME
// setup exception handling
executionExceptionHandler = SOPExecutionExceptionHandler()
exitCodeExceptionMapper = SOPExceptionExitCodeMapper()
isCaseInsensitiveEnumValuesAllowed = true
}
.execute(*args)
}
}
/**
* Control the locale.
*
* @see <a href="https://picocli.info/#_controlling_the_locale">Picocli Readme</a>
*/
@Command
class InitLocale {
@Option(names = ["-l", "--locale"], descriptionKey = "sop.locale")
fun setLocale(locale: String) = Locale.setDefault(Locale(locale))
@Unmatched
var remainder: MutableList<String> =
mutableListOf() // ignore any other parameters and options in the first parsing phase
}
}

View file

@ -1,98 +0,0 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli
import java.util.*
import kotlin.system.exitProcess
import picocli.AutoComplete
import picocli.CommandLine
import sop.SOPV
import sop.cli.picocli.commands.*
import sop.exception.SOPGPException
@CommandLine.Command(
name = "sopv",
resourceBundle = "msg_sop",
exitCodeOnInvalidInput = SOPGPException.UnsupportedSubcommand.EXIT_CODE,
subcommands =
[
// Meta subcommands
VersionCmd::class,
// signature verification subcommands
VerifyCmd::class,
InlineVerifyCmd::class,
// misc
CommandLine.HelpCommand::class,
AutoComplete.GenerateCompletion::class])
class SopVCLI {
companion object {
@JvmStatic private var sopvInstance: SOPV? = null
@JvmStatic
fun getSopV(): SOPV =
checkNotNull(sopvInstance) { cliMsg.getString("sop.error.runtime.no_backend_set") }
@JvmStatic
fun setSopVInstance(sopv: SOPV?) {
sopvInstance = sopv
}
@JvmField var cliMsg: ResourceBundle = ResourceBundle.getBundle("msg_sop")
@JvmField var EXECUTABLE_NAME = "sopv"
@JvmField
@CommandLine.Option(names = ["--stacktrace"], scope = CommandLine.ScopeType.INHERIT)
var stacktrace = false
@JvmStatic
fun main(vararg args: String) {
val exitCode = execute(*args)
if (exitCode != 0) {
exitProcess(exitCode)
}
}
@JvmStatic
fun execute(vararg args: String): Int {
// Set locale
CommandLine(InitLocale()).parseArgs(*args)
// Re-set bundle with updated locale
cliMsg = ResourceBundle.getBundle("msg_sop")
return CommandLine(SopVCLI::class.java)
.apply {
// explicitly set help command resource bundle
subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help"))
// Hide generate-completion command
subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true)
// overwrite executable name
commandName = EXECUTABLE_NAME
// setup exception handling
executionExceptionHandler = SOPExecutionExceptionHandler()
exitCodeExceptionMapper = SOPExceptionExitCodeMapper()
isCaseInsensitiveEnumValuesAllowed = true
}
.execute(*args)
}
}
/**
* Control the locale.
*
* @see <a href="https://picocli.info/#_controlling_the_locale">Picocli Readme</a>
*/
@CommandLine.Command
class InitLocale {
@CommandLine.Option(names = ["-l", "--locale"], descriptionKey = "sop.locale")
fun setLocale(locale: String) = Locale.setDefault(Locale(locale))
@CommandLine.Unmatched
var remainder: MutableList<String> =
mutableListOf() // ignore any other parameters and options in the first parsing phase
}
}

View file

@ -1,248 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.*
import java.text.ParseException
import java.util.*
import sop.cli.picocli.commands.AbstractSopCmd.EnvironmentVariableResolver
import sop.exception.SOPGPException.*
import sop.util.UTCUtil.Companion.parseUTCDate
import sop.util.UTF8Util.Companion.decodeUTF8
/** Abstract super class of SOP subcommands. */
abstract class AbstractSopCmd(locale: Locale = Locale.getDefault()) : Runnable {
private val messages: ResourceBundle = ResourceBundle.getBundle("msg_sop", locale)
var environmentVariableResolver = EnvironmentVariableResolver { name: String ->
System.getenv(name)
}
/** Interface to modularize resolving of environment variables. */
fun interface EnvironmentVariableResolver {
/**
* Resolve the value of the given environment variable. Return null if the variable is not
* present.
*
* @param name name of the variable
* @return variable value or null
*/
fun resolveEnvironmentVariable(name: String): String?
}
fun throwIfOutputExists(output: String?) {
output
?.let { File(it) }
?.let {
if (it.exists()) {
val errorMsg: String =
getMsg(
"sop.error.indirect_data_type.output_file_already_exists",
it.absolutePath)
throw OutputExists(errorMsg)
}
}
}
fun getMsg(key: String): String = messages.getString(key)
fun getMsg(key: String, vararg args: String): String {
val msg = messages.getString(key)
return String.format(msg, *args)
}
fun throwIfMissingArg(arg: Any?, argName: String) {
if (arg == null) {
val errorMsg = getMsg("sop.error.usage.argument_required", argName)
throw MissingArg(errorMsg)
}
}
fun throwIfEmptyParameters(arg: Collection<*>, parmName: String) {
if (arg.isEmpty()) {
val errorMsg = getMsg("sop.error.usage.parameter_required", parmName)
throw MissingArg(errorMsg)
}
}
fun <T> throwIfUnsupportedSubcommand(subcommand: T?, subcommandName: String): T {
if (subcommand == null) {
val errorMsg =
getMsg("sop.error.feature_support.subcommand_not_supported", subcommandName)
throw UnsupportedSubcommand(errorMsg)
}
return subcommand
}
@Throws(IOException::class)
fun getInput(indirectInput: String): InputStream {
val trimmed = indirectInput.trim()
require(trimmed.isNotBlank()) { "Input cannot be blank." }
if (trimmed.startsWith(PRFX_ENV)) {
if (File(trimmed).exists()) {
val errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed)
throw AmbiguousInput(errorMsg)
}
val envName = trimmed.substring(PRFX_ENV.length)
val envValue = environmentVariableResolver.resolveEnvironmentVariable(envName)
requireNotNull(envValue) {
getMsg("sop.error.indirect_data_type.environment_variable_not_set", envName)
}
require(envValue.trim().isNotEmpty()) {
getMsg("sop.error.indirect_data_type.environment_variable_empty", envName)
}
return envValue.byteInputStream()
} else if (trimmed.startsWith(PRFX_FD)) {
if (File(trimmed).exists()) {
val errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed)
throw AmbiguousInput(errorMsg)
}
val fdFile: File = fileDescriptorFromString(trimmed)
return try {
fdFile.inputStream()
} catch (e: FileNotFoundException) {
val errorMsg =
getMsg(
"sop.error.indirect_data_type.file_descriptor_not_found",
fdFile.absolutePath)
throw IOException(errorMsg, e)
}
} else {
val file = File(trimmed)
if (!file.exists()) {
val errorMsg =
getMsg(
"sop.error.indirect_data_type.input_file_does_not_exist", file.absolutePath)
throw MissingInput(errorMsg)
}
if (!file.isFile()) {
val errorMsg =
getMsg("sop.error.indirect_data_type.input_not_a_file", file.absolutePath)
throw MissingInput(errorMsg)
}
return file.inputStream()
}
}
@Throws(IOException::class)
fun getOutput(indirectOutput: String?): OutputStream {
requireNotNull(indirectOutput) { "Output cannot be null." }
val trimmed = indirectOutput.trim()
require(trimmed.isNotEmpty()) { "Output cannot be blank." }
// @ENV not allowed for output
if (trimmed.startsWith(PRFX_ENV)) {
val errorMsg = getMsg("sop.error.indirect_data_type.illegal_use_of_env_designator")
throw UnsupportedSpecialPrefix(errorMsg)
}
// File Descriptor
if (trimmed.startsWith(PRFX_FD)) {
val fdFile = fileDescriptorFromString(trimmed)
return try {
fdFile.outputStream()
} catch (e: FileNotFoundException) {
val errorMsg =
getMsg(
"sop.error.indirect_data_type.file_descriptor_not_found",
fdFile.absolutePath)
throw IOException(errorMsg, e)
}
}
val file = File(trimmed)
if (file.exists()) {
val errorMsg =
getMsg("sop.error.indirect_data_type.output_file_already_exists", file.absolutePath)
throw OutputExists(errorMsg)
}
if (!file.createNewFile()) {
val errorMsg =
getMsg(
"sop.error.indirect_data_type.output_file_cannot_be_created", file.absolutePath)
throw IOException(errorMsg)
}
return file.outputStream()
}
fun fileDescriptorFromString(fdString: String): File {
val fdDir = File("/dev/fd/")
if (!fdDir.exists()) {
val errorMsg = getMsg("sop.error.indirect_data_type.designator_fd_not_supported")
throw UnsupportedSpecialPrefix(errorMsg)
}
val fdNumber = fdString.substring(PRFX_FD.length)
require(PATTERN_FD.matcher(fdNumber).matches()) {
"File descriptor must be a positive number."
}
return File(fdDir, fdNumber)
}
fun parseNotAfter(notAfter: String): Date {
return when (notAfter) {
"now" -> Date()
"-" -> END_OF_TIME
else ->
try {
parseUTCDate(notAfter)
} catch (e: ParseException) {
val errorMsg = getMsg("sop.error.input.malformed_not_after")
throw IllegalArgumentException(errorMsg)
}
}
}
fun parseNotBefore(notBefore: String): Date {
return when (notBefore) {
"now" -> Date()
"-" -> DAWN_OF_TIME
else ->
try {
parseUTCDate(notBefore)
} catch (e: ParseException) {
val errorMsg = getMsg("sop.error.input.malformed_not_before")
throw IllegalArgumentException(errorMsg)
}
}
}
companion object {
const val PRFX_ENV = "@ENV:"
const val PRFX_FD = "@FD:"
@JvmField val DAWN_OF_TIME = Date(0)
@JvmField
@Deprecated("Replace with DAWN_OF_TIME", ReplaceWith("DAWN_OF_TIME"))
val BEGINNING_OF_TIME = DAWN_OF_TIME
@JvmField val END_OF_TIME = Date(8640000000000000L)
@JvmField val PATTERN_FD = "^\\d{1,20}$".toPattern()
@Throws(IOException::class)
@JvmStatic
fun stringFromInputStream(inputStream: InputStream): String {
return inputStream.use { input ->
val byteOut = ByteArrayOutputStream()
val buf = ByteArray(4096)
var read: Int
while (input.read(buf).also { read = it } != -1) {
byteOut.write(buf, 0, read)
}
// TODO: For decrypt operations we MUST accept non-UTF8 passwords
decodeUTF8(byteOut.toByteArray())
}
}
}
}

View file

@ -1,32 +0,0 @@
// SPDX-FileCopyrightText: 2023 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 sop.cli.picocli.SopCLI
import sop.exception.SOPGPException.BadData
import sop.exception.SOPGPException.UnsupportedOption
@Command(
name = "armor",
resourceBundle = "msg_armor",
exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE)
class ArmorCmd : AbstractSopCmd() {
override fun run() {
val armor = throwIfUnsupportedSubcommand(SopCLI.getSop().armor(), "armor")
try {
val ready = armor.data(System.`in`)
ready.writeTo(System.out)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data")
throw BadData(errorMsg, badData)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -1,46 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import java.lang.RuntimeException
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException
@Command(
name = "change-key-password",
resourceBundle = "msg_change-key-password",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
class ChangeKeyPasswordCmd : AbstractSopCmd() {
@Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true
@Option(names = ["--old-key-password"], paramLabel = "PASSWORD")
var oldKeyPasswords: List<String> = listOf()
@Option(names = ["--new-key-password"], arity = "0..1", paramLabel = "PASSWORD")
var newKeyPassword: String? = null
override fun run() {
val changeKeyPassword =
throwIfUnsupportedSubcommand(SopCLI.getSop().changeKeyPassword(), "change-key-password")
if (!armor) {
changeKeyPassword.noArmor()
}
oldKeyPasswords.forEach { changeKeyPassword.oldKeyPassphrase(it) }
newKeyPassword?.let { changeKeyPassword.newKeyPassphrase(it) }
try {
changeKeyPassword.keys(System.`in`).writeTo(System.out)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -1,41 +0,0 @@
// SPDX-FileCopyrightText: 2023 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 sop.cli.picocli.SopCLI
import sop.exception.SOPGPException
import sop.exception.SOPGPException.BadData
@Command(
name = "dearmor",
resourceBundle = "msg_dearmor",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
class DearmorCmd : AbstractSopCmd() {
override fun run() {
val dearmor = throwIfUnsupportedSubcommand(SopCLI.getSop().dearmor(), "dearmor")
try {
dearmor.data(System.`in`).writeTo(System.out)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data")
throw BadData(errorMsg, badData)
} catch (e: IOException) {
e.message?.let {
val errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data")
if (it == "invalid armor" ||
it == "invalid armor header" ||
it == "inconsistent line endings in headers" ||
it.startsWith("unable to decode base64 data")) {
throw BadData(errorMsg, e)
}
throw RuntimeException(e)
}
?: throw RuntimeException(e)
}
}
}

View file

@ -1,224 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import java.io.PrintWriter
import picocli.CommandLine.*
import sop.DecryptionResult
import sop.SessionKey
import sop.SessionKey.Companion.fromString
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException.*
import sop.operation.Decrypt
@Command(
name = "decrypt",
resourceBundle = "msg_decrypt",
exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE)
class DecryptCmd : AbstractSopCmd() {
@Option(names = [OPT_SESSION_KEY_OUT], paramLabel = "SESSIONKEY")
var sessionKeyOut: String? = null
@Option(names = [OPT_WITH_SESSION_KEY], paramLabel = "SESSIONKEY")
var withSessionKey: List<String> = listOf()
@Option(names = [OPT_WITH_PASSWORD], paramLabel = "PASSWORD")
var withPassword: List<String> = listOf()
@Option(names = [OPT_VERIFICATIONS_OUT], paramLabel = "VERIFICATIONS")
var verifyOut: String? = null
@Option(names = [OPT_VERIFY_WITH], paramLabel = "CERT") var certs: List<String> = listOf()
@Option(names = [OPT_NOT_BEFORE], paramLabel = "DATE") var notBefore = "-"
@Option(names = [OPT_NOT_AFTER], paramLabel = "DATE") var notAfter = "now"
@Parameters(index = "0..*", paramLabel = "KEY") var keys: List<String> = listOf()
@Option(names = [OPT_WITH_KEY_PASSWORD], paramLabel = "PASSWORD")
var withKeyPassword: List<String> = listOf()
override fun run() {
val decrypt = throwIfUnsupportedSubcommand(SopCLI.getSop().decrypt(), "decrypt")
throwIfOutputExists(verifyOut)
throwIfOutputExists(sessionKeyOut)
setNotAfter(notAfter, decrypt)
setNotBefore(notBefore, decrypt)
setWithPasswords(withPassword, decrypt)
setWithSessionKeys(withSessionKey, decrypt)
setWithKeyPassword(withKeyPassword, decrypt)
setVerifyWith(certs, decrypt)
setDecryptWith(keys, decrypt)
if (verifyOut != null && certs.isEmpty()) {
val errorMsg =
getMsg(
"sop.error.usage.option_requires_other_option",
OPT_VERIFICATIONS_OUT,
OPT_VERIFY_WITH)
throw IncompleteVerification(errorMsg)
}
try {
val ready = decrypt.ciphertext(System.`in`)
val result = ready.writeTo(System.out)
writeSessionKeyOut(result)
writeVerifyOut(result)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.stdin_not_a_message")
throw BadData(errorMsg, badData)
} catch (e: CannotDecrypt) {
val errorMsg = getMsg("sop.error.runtime.cannot_decrypt_message")
throw CannotDecrypt(errorMsg, e)
} catch (ioException: IOException) {
throw RuntimeException(ioException)
}
}
@Throws(IOException::class)
private fun writeVerifyOut(result: DecryptionResult) {
verifyOut?.let {
getOutput(it).use { out ->
PrintWriter(out).use { pw ->
result.verifications.forEach { verification -> pw.println(verification) }
}
}
}
}
@Throws(IOException::class)
private fun writeSessionKeyOut(result: DecryptionResult) {
sessionKeyOut?.let { fileName ->
getOutput(fileName).use { out ->
if (!result.sessionKey.isPresent) {
val errorMsg = getMsg("sop.error.runtime.no_session_key_extracted")
throw UnsupportedOption(String.format(errorMsg, OPT_SESSION_KEY_OUT))
}
PrintWriter(out).use { it.println(result.sessionKey.get()!!) }
}
}
}
private fun setDecryptWith(keys: List<String>, decrypt: Decrypt) {
for (key in keys) {
try {
getInput(key).use { decrypt.withKey(it) }
} catch (keyIsProtected: KeyIsProtected) {
val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", key)
throw KeyIsProtected(errorMsg, keyIsProtected)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_private_key", key)
throw BadData(errorMsg, badData)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}
private fun setVerifyWith(certs: List<String>, decrypt: Decrypt) {
for (cert in certs) {
try {
getInput(cert).use { certIn -> decrypt.verifyWithCert(certIn) }
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_certificate", cert)
throw BadData(errorMsg, badData)
} catch (ioException: IOException) {
throw RuntimeException(ioException)
}
}
}
private fun setWithSessionKeys(withSessionKey: List<String>, decrypt: Decrypt) {
for (sessionKeyFile in withSessionKey) {
val sessionKeyString: String =
try {
stringFromInputStream(getInput(sessionKeyFile))
} catch (e: IOException) {
throw RuntimeException(e)
}
val sessionKey: SessionKey =
try {
fromString(sessionKeyString)
} catch (e: IllegalArgumentException) {
val errorMsg = getMsg("sop.error.input.malformed_session_key")
throw IllegalArgumentException(errorMsg, e)
}
try {
decrypt.withSessionKey(sessionKey)
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg =
getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_SESSION_KEY)
throw UnsupportedOption(errorMsg, unsupportedOption)
}
}
}
private fun setWithPasswords(withPassword: List<String>, decrypt: Decrypt) {
for (passwordFile in withPassword) {
try {
val password = stringFromInputStream(getInput(passwordFile))
decrypt.withPassword(password)
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg =
getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_PASSWORD)
throw UnsupportedOption(errorMsg, unsupportedOption)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}
private fun setWithKeyPassword(withKeyPassword: List<String>, decrypt: Decrypt) {
for (passwordFile in withKeyPassword) {
try {
val password = stringFromInputStream(getInput(passwordFile))
decrypt.withKeyPassword(password)
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg =
getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_KEY_PASSWORD)
throw UnsupportedOption(errorMsg, unsupportedOption)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}
private fun setNotAfter(notAfter: String, decrypt: Decrypt) {
val notAfterDate = parseNotAfter(notAfter)
try {
decrypt.verifyNotAfter(notAfterDate)
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_AFTER)
throw UnsupportedOption(errorMsg, unsupportedOption)
}
}
private fun setNotBefore(notBefore: String, decrypt: Decrypt) {
val notBeforeDate = parseNotBefore(notBefore)
try {
decrypt.verifyNotBefore(notBeforeDate)
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_BEFORE)
throw UnsupportedOption(errorMsg, unsupportedOption)
}
}
companion object {
const val OPT_SESSION_KEY_OUT = "--session-key-out"
const val OPT_WITH_SESSION_KEY = "--with-session-key"
const val OPT_WITH_PASSWORD = "--with-password"
const val OPT_WITH_KEY_PASSWORD = "--with-key-password"
const val OPT_VERIFICATIONS_OUT = "--verifications-out"
const val OPT_VERIFY_WITH = "--verify-with"
const val OPT_NOT_BEFORE = "--verify-not-before"
const val OPT_NOT_AFTER = "--verify-not-after"
}
}

View file

@ -1,159 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import java.io.PrintWriter
import picocli.CommandLine.*
import sop.cli.picocli.SopCLI
import sop.enums.EncryptAs
import sop.exception.SOPGPException.*
@Command(
name = "encrypt",
resourceBundle = "msg_encrypt",
exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE)
class EncryptCmd : AbstractSopCmd() {
@Option(names = ["--no-armor"], negatable = true) var armor = true
@Option(names = ["--as"], paramLabel = "{binary|text}") var type: EncryptAs? = null
@Option(names = ["--with-password"], paramLabel = "PASSWORD")
var withPassword: List<String> = listOf()
@Option(names = ["--sign-with"], paramLabel = "KEY") var signWith: List<String> = listOf()
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
var withKeyPassword: List<String> = listOf()
@Option(names = ["--profile"], paramLabel = "PROFILE") var profile: String? = null
@Parameters(index = "0..*", paramLabel = "CERTS") var certs: List<String> = listOf()
@Option(names = ["--session-key-out"], paramLabel = "SESSIONKEY")
var sessionKeyOut: String? = null
override fun run() {
val encrypt = throwIfUnsupportedSubcommand(SopCLI.getSop().encrypt(), "encrypt")
throwIfOutputExists(sessionKeyOut)
profile?.let {
try {
encrypt.profile(it)
} catch (e: UnsupportedProfile) {
val errorMsg = getMsg("sop.error.usage.profile_not_supported", "encrypt", it)
throw UnsupportedProfile(errorMsg, e)
}
}
type?.let {
try {
encrypt.mode(it)
} catch (e: UnsupportedOption) {
val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as")
throw UnsupportedOption(errorMsg, e)
}
}
if (withPassword.isEmpty() && certs.isEmpty()) {
val errorMsg = getMsg("sop.error.usage.password_or_cert_required")
throw MissingArg(errorMsg)
}
for (passwordFileName in withPassword) {
try {
val password = stringFromInputStream(getInput(passwordFileName))
encrypt.withPassword(password)
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg =
getMsg("sop.error.feature_support.option_not_supported", "--with-password")
throw UnsupportedOption(errorMsg, unsupportedOption)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
for (passwordFileName in withKeyPassword) {
try {
val password = stringFromInputStream(getInput(passwordFileName))
encrypt.withKeyPassword(password)
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg =
getMsg("sop.error.feature_support.option_not_supported", "--with-key-password")
throw UnsupportedOption(errorMsg, unsupportedOption)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
for (keyInput in signWith) {
try {
getInput(keyInput).use { keyIn -> encrypt.signWith(keyIn) }
} catch (e: IOException) {
throw RuntimeException(e)
} catch (keyIsProtected: KeyIsProtected) {
val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput)
throw KeyIsProtected(errorMsg, keyIsProtected)
} catch (unsupportedAsymmetricAlgo: UnsupportedAsymmetricAlgo) {
val errorMsg =
getMsg("sop.error.runtime.key_uses_unsupported_asymmetric_algorithm", keyInput)
throw UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo)
} catch (keyCannotSign: KeyCannotSign) {
val errorMsg = getMsg("sop.error.runtime.key_cannot_sign", keyInput)
throw KeyCannotSign(errorMsg, keyCannotSign)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput)
throw BadData(errorMsg, badData)
}
}
for (certInput in certs) {
try {
getInput(certInput).use { certIn -> encrypt.withCert(certIn) }
} catch (e: IOException) {
throw RuntimeException(e)
} catch (unsupportedAsymmetricAlgo: UnsupportedAsymmetricAlgo) {
val errorMsg =
getMsg(
"sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput)
throw UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo)
} catch (certCannotEncrypt: CertCannotEncrypt) {
val errorMsg = getMsg("sop.error.runtime.cert_cannot_encrypt", certInput)
throw CertCannotEncrypt(errorMsg, certCannotEncrypt)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput)
throw BadData(errorMsg, badData)
}
}
if (!armor) {
encrypt.noArmor()
}
try {
val ready = encrypt.plaintext(System.`in`)
val result = ready.writeTo(System.out)
if (sessionKeyOut == null) {
return
}
getOutput(sessionKeyOut).use {
if (!result.sessionKey.isPresent) {
val errorMsg = getMsg("sop.error.runtime.no_session_key_extracted")
throw UnsupportedOption(String.format(errorMsg, "--session-key-out"))
}
val sessionKey = result.sessionKey.get() ?: return
val writer = PrintWriter(it)
writer.println(sessionKey)
writer.flush()
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -1,40 +0,0 @@
// SPDX-FileCopyrightText: 2023 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
import sop.exception.SOPGPException.BadData
@Command(
name = "extract-cert",
resourceBundle = "msg_extract-cert",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
class ExtractCertCmd : AbstractSopCmd() {
@Option(names = ["--no-armor"], negatable = true) var armor = true
override fun run() {
val extractCert =
throwIfUnsupportedSubcommand(SopCLI.getSop().extractCert(), "extract-cert")
if (!armor) {
extractCert.noArmor()
}
try {
val ready = extractCert.key(System.`in`)
ready.writeTo(System.out)
} catch (e: IOException) {
throw RuntimeException(e)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.stdin_not_a_private_key")
throw BadData(errorMsg, badData)
}
}
}

View file

@ -1,76 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import picocli.CommandLine.*
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException.UnsupportedOption
import sop.exception.SOPGPException.UnsupportedProfile
@Command(
name = "generate-key",
resourceBundle = "msg_generate-key",
exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE)
class GenerateKeyCmd : AbstractSopCmd() {
@Option(names = ["--no-armor"], negatable = true) var armor = true
@Parameters(paramLabel = "USERID") var userId: List<String> = listOf()
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
var withKeyPassword: String? = null
@Option(names = ["--profile"], paramLabel = "PROFILE") var profile: String? = null
@Option(names = ["--signing-only"]) var signingOnly: Boolean = false
override fun run() {
val generateKey =
throwIfUnsupportedSubcommand(SopCLI.getSop().generateKey(), "generate-key")
profile?.let {
try {
generateKey.profile(it)
} catch (e: UnsupportedProfile) {
val errorMsg =
getMsg("sop.error.usage.profile_not_supported", "generate-key", profile!!)
throw UnsupportedProfile(errorMsg, e)
}
}
if (signingOnly) {
generateKey.signingOnly()
}
for (userId in userId) {
generateKey.userId(userId)
}
if (!armor) {
generateKey.noArmor()
}
withKeyPassword?.let {
try {
val password = stringFromInputStream(getInput(it))
generateKey.withKeyPassword(password)
} catch (e: UnsupportedOption) {
val errorMsg =
getMsg("sop.error.feature_support.option_not_supported", "--with-key-password")
throw UnsupportedOption(errorMsg, e)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
try {
val ready = generateKey.generate()
ready.writeTo(System.out)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -1,47 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import java.lang.RuntimeException
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException
@Command(
name = "inline-detach",
resourceBundle = "msg_inline-detach",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
class InlineDetachCmd : AbstractSopCmd() {
@Option(names = ["--signatures-out"], paramLabel = "SIGNATURES")
var signaturesOut: String? = null
@Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true
override fun run() {
val inlineDetach =
throwIfUnsupportedSubcommand(SopCLI.getSop().inlineDetach(), "inline-detach")
throwIfOutputExists(signaturesOut)
throwIfMissingArg(signaturesOut, "--signatures-out")
if (!armor) {
inlineDetach.noArmor()
}
try {
getOutput(signaturesOut).use { sigOut ->
inlineDetach
.message(System.`in`)
.writeTo(System.out) // message out
.writeTo(sigOut) // signatures out
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -1,89 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import picocli.CommandLine.*
import sop.cli.picocli.SopCLI
import sop.enums.InlineSignAs
import sop.exception.SOPGPException.*
@Command(
name = "inline-sign",
resourceBundle = "msg_inline-sign",
exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE)
class InlineSignCmd : AbstractSopCmd() {
@Option(names = ["--no-armor"], negatable = true) var armor = true
@Option(names = ["--as"], paramLabel = "{binary|text|clearsigned}")
var type: InlineSignAs? = null
@Parameters(paramLabel = "KEYS") var secretKeyFile: List<String> = listOf()
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
var withKeyPassword: List<String> = listOf()
override fun run() {
val inlineSign = throwIfUnsupportedSubcommand(SopCLI.getSop().inlineSign(), "inline-sign")
if (!armor && type == InlineSignAs.clearsigned) {
val errorMsg = getMsg("sop.error.usage.incompatible_options.clearsigned_no_armor")
throw IncompatibleOptions(errorMsg)
}
type?.let {
try {
inlineSign.mode(it)
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as")
throw UnsupportedOption(errorMsg, unsupportedOption)
}
}
if (secretKeyFile.isEmpty()) {
val errorMsg = getMsg("sop.error.usage.parameter_required", "KEYS")
throw MissingArg(errorMsg)
}
for (passwordFile in withKeyPassword) {
try {
val password = stringFromInputStream(getInput(passwordFile))
inlineSign.withKeyPassword(password)
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg =
getMsg("sop.error.feature_support.option_not_supported", "--with-key-password")
throw UnsupportedOption(errorMsg, unsupportedOption)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
for (keyInput in secretKeyFile) {
try {
getInput(keyInput).use { keyIn -> inlineSign.key(keyIn) }
} catch (e: IOException) {
throw RuntimeException(e)
} catch (e: KeyIsProtected) {
val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput)
throw KeyIsProtected(errorMsg, e)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput)
throw BadData(errorMsg, badData)
}
}
if (!armor) {
inlineSign.noArmor()
}
try {
val ready = inlineSign.data(System.`in`)
ready.writeTo(System.out)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -1,93 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import java.io.PrintWriter
import picocli.CommandLine.*
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException.*
@Command(
name = "inline-verify",
resourceBundle = "msg_inline-verify",
exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE)
class InlineVerifyCmd : AbstractSopCmd() {
@Parameters(arity = "0..*", paramLabel = "CERT") var certificates: List<String> = listOf()
@Option(names = ["--not-before"], paramLabel = "DATE") var notBefore: String = "-"
@Option(names = ["--not-after"], paramLabel = "DATE") var notAfter: String = "now"
@Option(names = ["--verifications-out"], paramLabel = "VERIFICATIONS")
var verificationsOut: String? = null
override fun run() {
val inlineVerify =
throwIfUnsupportedSubcommand(SopCLI.getSop().inlineVerify(), "inline-verify")
throwIfOutputExists(verificationsOut)
try {
inlineVerify.notAfter(parseNotAfter(notAfter))
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after")
throw UnsupportedOption(errorMsg, unsupportedOption)
}
try {
inlineVerify.notBefore(parseNotBefore(notBefore))
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before")
throw UnsupportedOption(errorMsg, unsupportedOption)
}
for (certInput in certificates) {
try {
getInput(certInput).use { certIn -> inlineVerify.cert(certIn) }
} catch (ioException: IOException) {
throw RuntimeException(ioException)
} catch (unsupportedAsymmetricAlgo: UnsupportedAsymmetricAlgo) {
val errorMsg =
getMsg(
"sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput)
throw UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput)
throw BadData(errorMsg, badData)
}
}
val verifications =
try {
val ready = inlineVerify.data(System.`in`)
ready.writeTo(System.out)
} catch (e: NoSignature) {
val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found")
throw NoSignature(errorMsg, e)
} catch (ioException: IOException) {
throw RuntimeException(ioException)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.stdin_not_a_message")
throw BadData(errorMsg, badData)
}
verificationsOut?.let {
try {
getOutput(it).use { outputStream ->
val pw = PrintWriter(outputStream)
for (verification in verifications) {
pw.println(verification)
}
pw.flush()
pw.close()
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}
}

View file

@ -1,34 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import picocli.CommandLine.Command
import picocli.CommandLine.Parameters
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException
import sop.exception.SOPGPException.UnsupportedProfile
@Command(
name = "list-profiles",
resourceBundle = "msg_list-profiles",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
class ListProfilesCmd : AbstractSopCmd() {
@Parameters(paramLabel = "COMMAND", arity = "1", descriptionKey = "subcommand")
lateinit var subcommand: String
override fun run() {
val listProfiles =
throwIfUnsupportedSubcommand(SopCLI.getSop().listProfiles(), "list-profiles")
try {
listProfiles.subcommand(subcommand).forEach { println(it) }
} catch (e: UnsupportedProfile) {
val errorMsg =
getMsg("sop.error.feature_support.subcommand_does_not_support_profiles", subcommand)
throw UnsupportedProfile(errorMsg, e)
}
}
}

View file

@ -1,58 +0,0 @@
// SPDX-FileCopyrightText: 2023 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
import sop.exception.SOPGPException.KeyIsProtected
@Command(
name = "revoke-key",
resourceBundle = "msg_revoke-key",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
class RevokeKeyCmd : AbstractSopCmd() {
@Option(names = ["--no-armor"], negatable = true) var armor = true
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
var withKeyPassword: String? = null
override fun run() {
val revokeKey = throwIfUnsupportedSubcommand(SopCLI.getSop().revokeKey(), "revoke-key")
if (!armor) {
revokeKey.noArmor()
}
withKeyPassword?.let {
try {
val password = stringFromInputStream(getInput(it))
revokeKey.withKeyPassword(password)
} catch (e: SOPGPException.UnsupportedOption) {
val errorMsg =
getMsg("sop.error.feature_support.option_not_supported", "--with-key-password")
throw SOPGPException.UnsupportedOption(errorMsg, e)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
val ready =
try {
revokeKey.keys(System.`in`)
} catch (e: KeyIsProtected) {
val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", "STANDARD_IN")
throw KeyIsProtected(errorMsg, e)
}
try {
ready.writeTo(System.out)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -1,90 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import picocli.CommandLine.*
import sop.cli.picocli.SopCLI
import sop.enums.SignAs
import sop.exception.SOPGPException
import sop.exception.SOPGPException.BadData
import sop.exception.SOPGPException.KeyIsProtected
@Command(
name = "sign",
resourceBundle = "msg_detached-sign",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
class SignCmd : AbstractSopCmd() {
@Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true
@Option(names = ["--as"], paramLabel = "{binary|text}") var type: SignAs? = null
@Parameters(paramLabel = "KEYS") var secretKeyFile: List<String> = listOf()
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
var withKeyPassword: List<String> = listOf()
@Option(names = ["--micalg-out"], paramLabel = "MICALG") var micAlgOut: String? = null
override fun run() {
val detachedSign = throwIfUnsupportedSubcommand(SopCLI.getSop().detachedSign(), "sign")
throwIfOutputExists(micAlgOut)
throwIfEmptyParameters(secretKeyFile, "KEYS")
try {
type?.let { detachedSign.mode(it) }
} catch (unsupported: SOPGPException.UnsupportedOption) {
val errorMsg =
getMsg("sop.error.feature_support.option_not_supported", "--with-key-password")
throw SOPGPException.UnsupportedOption(errorMsg, unsupported)
} catch (ioe: IOException) {
throw RuntimeException(ioe)
}
withKeyPassword.forEach { passIn ->
try {
val password = stringFromInputStream(getInput(passIn))
detachedSign.withKeyPassword(password)
} catch (unsupported: SOPGPException.UnsupportedOption) {
val errorMsg =
getMsg("sop.error.feature_support.option_not_supported", "--with-key-password")
throw SOPGPException.UnsupportedOption(errorMsg, unsupported)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
secretKeyFile.forEach { keyIn ->
try {
getInput(keyIn).use { input -> detachedSign.key(input) }
} catch (ioe: IOException) {
throw RuntimeException(ioe)
} catch (keyIsProtected: KeyIsProtected) {
val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyIn)
throw KeyIsProtected(errorMsg, keyIsProtected)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_private_key", keyIn)
throw BadData(errorMsg, badData)
}
}
if (!armor) {
detachedSign.noArmor()
}
try {
val ready = detachedSign.data(System.`in`)
val result = ready.writeTo(System.out)
if (micAlgOut != null) {
getOutput(micAlgOut).use { result.micAlg.writeTo(it) }
}
} catch (e: IOException) {
throw java.lang.RuntimeException(e)
}
}
}

View file

@ -1,81 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import picocli.CommandLine.*
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException.*
@Command(
name = "verify",
resourceBundle = "msg_detached-verify",
exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE)
class VerifyCmd : AbstractSopCmd() {
@Parameters(index = "0", paramLabel = "SIGNATURE") lateinit var signature: String
@Parameters(index = "1..*", arity = "1..*", paramLabel = "CERT")
lateinit var certificates: List<String>
@Option(names = ["--not-before"], paramLabel = "DATE") var notBefore: String = "-"
@Option(names = ["--not-after"], paramLabel = "DATE") var notAfter: String = "now"
override fun run() {
val detachedVerify =
throwIfUnsupportedSubcommand(SopCLI.getSop().detachedVerify(), "verify")
try {
detachedVerify.notAfter(parseNotAfter(notAfter))
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after")
throw UnsupportedOption(errorMsg, unsupportedOption)
}
try {
detachedVerify.notBefore(parseNotBefore(notBefore))
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before")
throw UnsupportedOption(errorMsg, unsupportedOption)
}
for (certInput in certificates) {
try {
getInput(certInput).use { certIn -> detachedVerify.cert(certIn) }
} catch (ioException: IOException) {
throw RuntimeException(ioException)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput)
throw BadData(errorMsg, badData)
}
}
try {
getInput(signature).use { sigIn -> detachedVerify.signatures(sigIn) }
} catch (e: IOException) {
throw RuntimeException(e)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_signature", signature)
throw BadData(errorMsg, badData)
}
val verifications =
try {
detachedVerify.data(System.`in`)
} catch (e: NoSignature) {
val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found")
throw NoSignature(errorMsg, e)
} catch (ioException: IOException) {
throw RuntimeException(ioException)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.stdin_not_a_message")
throw BadData(errorMsg, badData)
}
for (verification in verifications) {
println(verification.toString())
}
}
}

View file

@ -1,57 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import picocli.CommandLine.ArgGroup
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException
@Command(
name = "version",
resourceBundle = "msg_version",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
class VersionCmd : AbstractSopCmd() {
@ArgGroup var exclusive: Exclusive? = null
class Exclusive {
@Option(names = ["--extended"]) var extended: Boolean = false
@Option(names = ["--backend"]) var backend: Boolean = false
@Option(names = ["--sop-spec"]) var sopSpec: Boolean = false
@Option(names = ["--sopv"]) var sopv: Boolean = false
}
override fun run() {
val version = throwIfUnsupportedSubcommand(SopCLI.getSop().version(), "version")
if (exclusive == null) {
// No option provided
println("${version.getName()} ${version.getVersion()}")
return
}
if (exclusive!!.extended) {
println(version.getExtendedVersion())
return
}
if (exclusive!!.backend) {
println(version.getBackendVersion())
return
}
if (exclusive!!.sopSpec) {
println(version.getSopSpecVersion())
return
}
if (exclusive!!.sopv) {
println(version.getSopVVersion())
return
}
}
}

View file

@ -1,11 +0,0 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Add ASCII Armor to standard input
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -1,11 +0,0 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Schütze Standard-Eingabe mit ASCII Armor
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -1,21 +0,0 @@
# SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Update the password of a key
usage.description.0=Unlock all secret keys from STDIN using the given old passwords and emit them re-locked using the new password to STDOUT.
usage.description.1=If any (sub-) key cannot be unlocked, this operation will exit with error code 67.
no-armor=ASCII armor the output
new-key-password.0=New password to lock the keys with.
new-key-password.1=If no new password is passed in, the keys will be emitted unlocked.
new-key-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
old-key-password.0=Old passwords to unlock the keys with.
old-key-password.1=Multiple passwords can be passed in, which are tested sequentially to unlock locked subkeys.
old-key-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nDescription:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -1,21 +0,0 @@
# SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Ändere das Passwort eines Schlüssels
usage.description.0=Entsperre alle Schlüssel von Standard-Eingabe mithilfe der alten Passwörter und gebe sie mit dem neuen Passwort gesperrt auf Standard-Ausgabe aus.
usage.description.1=Falls einer oder mehrere (Unter-)Schlüssel nicht entsperrt werden können, gibt diese Operation den Fehlercode 67 aus.
no-armor=Schütze Ausgabe mit ASCII Armor
new-key-password.0=Neues Passwort zur Sperrung der Schlüssel.
new-key-password.1=Falls kein neues Passwort angegeben wird, werden die Schlüssel entsperrt ausgegeben.
new-key-password.2=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
old-key-password.0=Alte Passwörter zum Entsperren der Schlüssel.
old-key-password.1=Mehrere Passwortkandidaten können übergeben werden, welche der Reihe nach durchprobiert werden, um Unterschlüssel zu entsperren.
old-key-password.2=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nBeschreibung:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -1,11 +0,0 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Remove ASCII Armor from standard input
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -1,11 +0,0 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Entferne ASCII Armor von Standard-Eingabe
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -1,31 +0,0 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Decrypt a message
session-key-out=Can be used to learn the session key on successful decryption
with-session-key.0=Symmetric message key (session key).
with-session-key.1=Enables decryption of the "CIPHERTEXT" using the session key directly against the "SEIPD" packet.
with-session-key.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
with-password.0=Symmetric passphrase to decrypt the message with.
with-password.1=Enables decryption based on any "SKESK" packets in the "CIPHERTEXT".
with-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
verify-out=Emits signature verification status to the designated output
verify-with=Certificates for signature verification
verify-not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z)
verify-not-before.1=Reject signatures with a creation date not in range.
verify-not-before.2=Defaults to beginning of time ('-').
verify-not-after.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z)
verify-not-after.1=Reject signatures with a creation date not in range.
verify-not-after.2=Defaults to current system time ('now').
verify-not-after.3=Accepts special value '-' for end of time.
with-key-password.0=Passphrase to unlock the secret key(s).
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
KEY[0..*]=Secret keys to attempt decryption with
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

@ -1,31 +0,0 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Entschlüssle eine Nachricht
session-key-out=Extrahiere den Nachrichtenschlüssel nach erfolgreicher Entschlüsselung
with-session-key.0=Symmetrischer Nachrichtenschlüssel (Sitzungsschlüssel).
with-session-key.1=Ermöglicht direkte Entschlüsselung des im "CIPHERTEXT" enthaltenen "SEIPD" Paketes mithilfe des Nachrichtenschlüssels.
with-session-key.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
with-password.0=Symmetrisches Passwort zur Entschlüsselung der Nachricht.
with-password.1=Ermöglicht Entschlüsselung basierend auf im "CIPHERTEXT" enthaltenen "SKESK" Paketen.
with-password.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
verify-out=Schreibe Status der Signaturprüfung in angegebene Ausgabe
verify-with=Zertifikate zur Signaturprüfung
verify-not-before.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z)
verify-not-before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab.
verify-not-before.2=Standardmäßig: Anbeginn der Zeit ('-').
verify-not-after.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z)
verify-not-after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab.
verify-not-after.2=Standardmäßig: Aktueller Zeitpunkt ('now').
verify-not-after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten.
with-key-password.0=Passwort zum Entsperren der privaten Schlüssel
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
KEY[0..*]=Private Schlüssel zum Entschlüsseln der Nachricht
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -1,20 +0,0 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Create a detached message signature
no-armor=ASCII armor the output
as.0=Specify the output format of the signed message.
as.1=Defaults to 'binary'.
as.2=If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53.
with-key-password.0=Passphrase to unlock the secret key(s).
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
micalg-out=Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156).
KEYS[0..*]=Secret keys used for signing
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

@ -1,20 +0,0 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Erstelle abgetrennte Nachrichten-Signatur
no-armor=Schütze Ausgabe mit ASCII Armor
as.0=Bestimme Signaturformat der Nachricht.
as.1=Standardmäßig: 'binary'.
as.2=Ist die Standard-Eingabe nicht UTF-8 kodiert und '--as=text' gesetzt, so wird inline-sign Fehlercode 53 zurückgeben.
with-key-password.0=Passwort zum Entsperren des privaten Schlüssels
with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
micalg-out=Gibt den verwendeten Digest-Algorithmus an die angegebene Ausgabe in einer Form aus, die zum Auffüllen des micalg-Parameters für den PGP/MIME Content-Type (RFC3156) verwendet werden kann.
KEYS[0..*]=Private Signaturschlüssel
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -1,23 +0,0 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Verify a detached signature
usage.description=Verify a detached signature over some data from STDIN.
not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z)
not-before.1=Reject signatures with a creation date not in range.
not-before.2=Defaults to beginning of time ("-").
not-after.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z)
not-after.1=Reject signatures with a creation date not in range.
not-after.2=Defaults to current system time ("now").
not-after.3=Accepts special value "-" for end of time.
SIGNATURE[0]=Detached signature
CERT[1..*]=Public key certificates for signature verification
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nDescription:%n
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -1,23 +0,0 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Prüfe eine abgetrennte Nachrichten-Signatur
usage.description=Prüfe eine abgetrennte Signatur über eine Nachricht von Standard-Eingabe.
not-before.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z)
not-before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab.
not-before.2=Standardmäßig: Anbeginn der Zeit ('-').
not-after.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z)
not-after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab.
not-after.2=Standardmäßig: Aktueller Zeitpunkt ('now').
not-after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten.
SIGNATURE[0]=Abgetrennte Signatur
CERT[1..*]=Zertifikate (öffentliche Schlüssel) zur Signaturprüfung
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nBeschreibung:%n
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -1,21 +0,0 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Encrypt a message from standard input
no-armor=ASCII armor the output
as=Type of the input data. Defaults to 'binary'
with-password.0=Encrypt the message with a password.
with-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
sign-with=Sign the output with a private key
profile=Profile identifier to switch between profiles
with-key-password.0=Passphrase to unlock the secret key(s).
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
CERTS[0..*]=Certificates the message gets encrypted to
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

@ -1,21 +0,0 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Verschlüssle eine Nachricht von Standard-Eingabe
no-armor=Schütze Ausgabe mit ASCII Armor
as=Format der Nachricht. Standardmäßig 'binary'
with-password.0=Verschlüssle die Nachricht mit einem Passwort
with-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
sign-with=Signiere die Nachricht mit einem privaten Schlüssel
profile=Profil-Identifikator um zwischen Profilen zu wechseln
with-key-password.0=Passwort zum Entsperren der privaten Schlüssel
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
CERTS[0..*]=Zertifikate für die die Nachricht verschlüsselt werden soll
stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -1,14 +0,0 @@
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Extract a public key certificate from a secret key
usage.description=Read a secret key from STDIN and emit the public key certificate to STDOUT.
no-armor=ASCII armor the output
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nDescription:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n
usage.optionListHeading = %nOptions:%n
usage.footerHeading=Powered by picocli%n

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