mirror of
https://codeberg.org/PGPainless/sop-java.git
synced 2024-06-30 15:26:49 +02:00
Compare commits
No commits in common. "main" and "1.2.3" have entirely different histories.
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -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. -->
|
|
14
.reuse/dep5
14
.reuse/dep5
|
@ -13,17 +13,3 @@ Source: https://pgpainless.org
|
||||||
Files: gradle*
|
Files: gradle*
|
||||||
Copyright: 2015 the original author or authors.
|
Copyright: 2015 the original author or authors.
|
||||||
License: Apache-2.0
|
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
52
.travis.yml
Normal 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
|
|
@ -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]
|
|
|
@ -1,7 +0,0 @@
|
||||||
# Code is licensed properly
|
|
||||||
# See https://reuse.software/
|
|
||||||
steps:
|
|
||||||
reuse:
|
|
||||||
image: fsfe/reuse:latest
|
|
||||||
commands:
|
|
||||||
- reuse lint
|
|
135
CHANGELOG.md
135
CHANGELOG.md
|
@ -6,141 +6,6 @@ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
# Changelog
|
# 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
|
## 1.2.3
|
||||||
- Bump Mockito version to `4.5.1`
|
- Bump Mockito version to `4.5.1`
|
||||||
|
|
||||||
|
|
21
README.md
21
README.md
|
@ -6,18 +6,16 @@ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
# SOP for Java
|
# SOP for Java
|
||||||
|
|
||||||
[![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java)
|
[![Travis (.com)](https://travis-ci.com/pgpainless/sop-java.svg?branch=master)](https://travis-ci.com/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/)
|
[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/sop-java)](https://search.maven.org/artifact/org.pgpainless/sop-java)
|
||||||
[![Coverage Status](https://coveralls.io/repos/github/pgpainless/sop-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/sop-java?branch=main)
|
[![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)
|
[![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.
|
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.
|
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
|
## Modules
|
||||||
|
|
||||||
The repository contains the following modules:
|
The repository contains the following modules:
|
||||||
|
@ -25,16 +23,13 @@ The repository contains the following modules:
|
||||||
* [sop-java](/sop-java) defines a set of Java interfaces describing the Stateless OpenPGP Protocol.
|
* [sop-java](/sop-java) defines a set of Java interfaces describing the Stateless OpenPGP Protocol.
|
||||||
* [sop-java-picocli](/sop-java-picocli) contains a wrapper application that transforms the `sop-java` API into a command line application
|
* [sop-java-picocli](/sop-java-picocli) contains a wrapper application that transforms the `sop-java` API into a command line application
|
||||||
compatible with the SOP-CLI specification.
|
compatible with the SOP-CLI specification.
|
||||||
* [external-sop](/external-sop) contains an API implementation that can be used to forward API calls to a SOP executable,
|
|
||||||
allowing to delegate the implementation logic to an arbitrary SOP CLI implementation.
|
|
||||||
|
|
||||||
## Known Implementations
|
## Known Implementations
|
||||||
(Please expand!)
|
(Please expand!)
|
||||||
|
|
||||||
| Project | Description |
|
| Project | Description |
|
||||||
|-------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
|
|---------------------------------------------------------------------------------------|-----------------------------------------------|
|
||||||
| [pgpainless-sop](https://github.com/pgpainless/pgpainless/tree/main/pgpainless-sop) | Implementation of `sop-java` using PGPainless |
|
| [pgpainless-sop](https://github.com/pgpainless/pgpainless/tree/master/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` |
|
|
||||||
|
|
||||||
### Implementations in other languages
|
### Implementations in other languages
|
||||||
| Project | Language |
|
| Project | Language |
|
||||||
|
|
17
build.gradle
17
build.gradle
|
@ -19,8 +19,6 @@ buildscript {
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'ru.vyarus.animalsniffer' version '1.5.3'
|
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'
|
apply from: 'version.gradle'
|
||||||
|
@ -31,8 +29,6 @@ allprojects {
|
||||||
apply plugin: 'eclipse'
|
apply plugin: 'eclipse'
|
||||||
apply plugin: 'jacoco'
|
apply plugin: 'jacoco'
|
||||||
apply plugin: 'checkstyle'
|
apply plugin: 'checkstyle'
|
||||||
apply plugin: 'kotlin'
|
|
||||||
apply plugin: 'com.diffplug.spotless'
|
|
||||||
|
|
||||||
// For non-cli modules enable android api compatibility check
|
// For non-cli modules enable android api compatibility check
|
||||||
if (it.name.equals('sop-java')) {
|
if (it.name.equals('sop-java')) {
|
||||||
|
@ -57,12 +53,6 @@ allprojects {
|
||||||
toolVersion = '8.18'
|
toolVersion = '8.18'
|
||||||
}
|
}
|
||||||
|
|
||||||
spotless {
|
|
||||||
kotlin {
|
|
||||||
ktfmt().dropboxStyle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
group 'org.pgpainless'
|
group 'org.pgpainless'
|
||||||
description = "Stateless OpenPGP Protocol API for Java"
|
description = "Stateless OpenPGP Protocol API for Java"
|
||||||
version = shortVersion
|
version = shortVersion
|
||||||
|
@ -79,13 +69,6 @@ allprojects {
|
||||||
reproducibleFileOrder = true
|
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 {
|
project.ext {
|
||||||
rootConfigDir = new File(rootDir, 'config')
|
rootConfigDir = new File(rootDir, 'config')
|
||||||
gitCommit = getGitCommit()
|
gitCommit = getGitCommit()
|
||||||
|
|
|
@ -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`.
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
# SPDX-FileCopyrightText: 2023 Paul Schaub <info@pgpainless.org>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: CC0-1.0
|
|
||||||
|
|
||||||
config.json
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"backends": [
|
|
||||||
{
|
|
||||||
"name": "Sequoia-SOP",
|
|
||||||
"sop": "/usr/bin/sqop"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
|
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
|
@ -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 {
|
|
||||||
}
|
|
|
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
|
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
|
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
|
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
|
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
|
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
|
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
|
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,5 +1,5 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
257
gradlew
vendored
257
gradlew
vendored
|
@ -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");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with 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.
|
## Gradle start up script for UN*X
|
||||||
#
|
##
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
# Resolve links: $0 may be a link
|
||||||
app_path=$0
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
# Need this for daisy-chained symlinks.
|
while [ -h "$PRG" ] ; do
|
||||||
while
|
ls=`ls -ld "$PRG"`
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
[ -h "$app_path" ]
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
do
|
PRG="$link"
|
||||||
ls=$( ls -ld "$app_path" )
|
else
|
||||||
link=${ls#*' -> '}
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
case $link in #(
|
fi
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
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.
|
# 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"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD="maximum"
|
||||||
|
|
||||||
warn () {
|
warn () {
|
||||||
echo "$*"
|
echo "$*"
|
||||||
} >&2
|
}
|
||||||
|
|
||||||
die () {
|
die () {
|
||||||
echo
|
echo
|
||||||
echo "$*"
|
echo "$*"
|
||||||
echo
|
echo
|
||||||
exit 1
|
exit 1
|
||||||
} >&2
|
}
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
# OS specific support (must be 'true' or 'false').
|
||||||
cygwin=false
|
cygwin=false
|
||||||
msys=false
|
msys=false
|
||||||
darwin=false
|
darwin=false
|
||||||
nonstop=false
|
nonstop=false
|
||||||
case "$( uname )" in #(
|
case "`uname`" in
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
CYGWIN* )
|
||||||
Darwin* ) darwin=true ;; #(
|
cygwin=true
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
;;
|
||||||
NONSTOP* ) nonstop=true ;;
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MSYS* | MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
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 [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
else
|
else
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
fi
|
fi
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
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."
|
location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
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.
|
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
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
@ -140,95 +106,80 @@ location of your Java installation."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
case $MAX_FD in #(
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
max*)
|
if [ $? -eq 0 ] ; then
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
warn "Could not query maximum file descriptor limit"
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
esac
|
fi
|
||||||
case $MAX_FD in #(
|
ulimit -n $MAX_FD
|
||||||
'' | soft) :;; #(
|
if [ $? -ne 0 ] ; then
|
||||||
*)
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
ulimit -n "$MAX_FD" ||
|
fi
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
else
|
||||||
esac
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
# * args from the command line
|
if $darwin; then
|
||||||
# * the main class name
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
# * -classpath
|
fi
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if "$cygwin" || "$msys" ; then
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
for arg do
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
if
|
SEP=""
|
||||||
case $arg in #(
|
for dir in $ROOTDIRSRAW ; do
|
||||||
-*) false ;; # don't mess with options #(
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
SEP="|"
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
|
||||||
# Roll the args list around exactly as many times as the number of
|
|
||||||
# args, so each arg winds up back in the position where it started, but
|
|
||||||
# possibly modified.
|
|
||||||
#
|
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
|
||||||
# changing the positional parameters here affects neither the number of
|
|
||||||
# iterations, nor the values presented in `arg`.
|
|
||||||
shift # remove old arg
|
|
||||||
set -- "$@" "$arg" # push replacement arg
|
|
||||||
done
|
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
|
fi
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
# Escape application args
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
save () {
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
# double quotes to make sure that they get re-expanded; and
|
echo " "
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
set -- \
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
-classpath "$CLASSPATH" \
|
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
|
||||||
#
|
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
|
||||||
#
|
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
exec "$JAVACMD" "$@"
|
||||||
|
|
|
@ -5,6 +5,5 @@
|
||||||
rootProject.name = 'SOP-Java'
|
rootProject.name = 'SOP-Java'
|
||||||
|
|
||||||
include 'sop-java',
|
include 'sop-java',
|
||||||
'sop-java-picocli',
|
'sop-java-picocli'
|
||||||
'external-sop'
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
[![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)
|
[![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,
|
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.
|
armoring and de-armoring of data, as well as - of course - encryption/decryption of messages and creation/verification of signatures.
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'application'
|
id 'application'
|
||||||
id 'org.asciidoctor.jvm.convert' version '3.3.2'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -12,16 +11,18 @@ dependencies {
|
||||||
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||||
|
|
||||||
|
// Testing Exit Codes in JUnit
|
||||||
|
// https://todd.ginsberg.com/post/testing-system-exit/
|
||||||
|
testImplementation "com.ginsberg:junit5-system-exit:$junitSysExitVersion"
|
||||||
|
|
||||||
// Mocking Components
|
// Mocking Components
|
||||||
testImplementation "org.mockito:mockito-core:$mockitoVersion"
|
testImplementation "org.mockito:mockito-core:$mockitoVersion"
|
||||||
|
|
||||||
// SOP
|
// SOP
|
||||||
implementation(project(":sop-java"))
|
implementation(project(":sop-java"))
|
||||||
testImplementation(testFixtures(project(":sop-java")))
|
|
||||||
|
|
||||||
// CLI
|
// CLI
|
||||||
implementation "info.picocli:picocli:$picocliVersion"
|
implementation "info.picocli:picocli:$picocliVersion"
|
||||||
annotationProcessor "info.picocli:picocli-codegen:$picocliVersion"
|
|
||||||
|
|
||||||
// @Nonnull, @Nullable...
|
// @Nonnull, @Nullable...
|
||||||
implementation "com.google.code.findbugs:jsr305:$jsrVersion"
|
implementation "com.google.code.findbugs:jsr305:$jsrVersion"
|
||||||
|
@ -35,7 +36,6 @@ application {
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
dependsOn(":sop-java:jar")
|
dependsOn(":sop-java:jar")
|
||||||
duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
|
|
||||||
|
|
||||||
manifest {
|
manifest {
|
||||||
attributes 'Main-Class': "$mainClassName"
|
attributes 'Main-Class': "$mainClassName"
|
||||||
|
@ -49,25 +49,3 @@ jar {
|
||||||
exclude "META-INF/*.RSA"
|
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']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
115
sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java
Normal file
115
sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
sop-java-picocli/src/main/java/sop/cli/picocli/Print.java
Normal file
26
sop-java-picocli/src/main/java/sop/cli/picocli/Print.java
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
99
sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java
Normal file
99
sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
Loading…
Reference in a new issue