mirror of
https://codeberg.org/PGPainless/sop-java.git
synced 2024-09-26 18:19:32 +02:00
Compare commits
132 commits
Author | SHA1 | Date | |
---|---|---|---|
Paul Schaub | 5f18edd2e7 | ||
Paul Schaub | 8134e3cd12 | ||
Paul Schaub | 8843d217e4 | ||
Paul Schaub | 61f3bf14c5 | ||
Paul Schaub | ed9fb31549 | ||
Paul Schaub | 8422a23396 | ||
Paul Schaub | ce21e07fc2 | ||
Paul Schaub | ec90d2e951 | ||
Paul Schaub | d5c011ea4a | ||
Paul Schaub | 1d80ff1d8d | ||
Paul Schaub | 9356447226 | ||
Paul Schaub | a13f1e2a0d | ||
Paul Schaub | e39cc7f0ac | ||
Paul Schaub | cbbdd09472 | ||
Paul Schaub | da6cba1d55 | ||
Paul Schaub | 4b2875d572 | ||
Paul Schaub | 30f7ca90cd | ||
Paul Schaub | bfa97aede8 | ||
Paul Schaub | bdbc9593c8 | ||
Paul Schaub | 3643aff082 | ||
Paul Schaub | ed9b2f5fef | ||
Paul Schaub | cd208c8942 | ||
Paul Schaub | 7325cad696 | ||
Paul Schaub | 7a825c7607 | ||
Paul Schaub | 03f8950b16 | ||
Paul Schaub | d5d7d67d6f | ||
Paul Schaub | e2a568e73e | ||
Paul Schaub | 7092baee4f | ||
Paul Schaub | 592aecd646 | ||
Paul Schaub | e5e64003f3 | ||
Paul Schaub | 51d9c29837 | ||
Paul Schaub | ae83ddcff6 | ||
Paul Schaub | 7eeb159f12 | ||
Paul Schaub | 60758dfa2f | ||
Paul Schaub | 6c952efca2 | ||
Paul Schaub | 3eaae149b7 | ||
Paul Schaub | 832a455c4c | ||
Paul Schaub | f2204dfd4d | ||
Paul Schaub | 8dc51b67a3 | ||
Paul Schaub | 7be71494cf | ||
Paul Schaub | f181453004 | ||
Paul Schaub | 9b79a49bb5 | ||
Paul Schaub | 01abae4d08 | ||
Paul Schaub | c53c69f3ac | ||
Paul Schaub | 4a405f6d39 | ||
Paul Schaub | 9cd9f151c9 | ||
Paul Schaub | 03da9bbfb7 | ||
Paul Schaub | da2b299f4d | ||
Paul Schaub | d149aac56c | ||
Paul Schaub | 6771952618 | ||
Paul Schaub | 1c0666b4e1 | ||
Paul Schaub | d24ff9cbde | ||
Paul Schaub | 802bc0aa73 | ||
Paul Schaub | 03cabdf3fb | ||
Paul Schaub | 3dde174880 | ||
Paul Schaub | 2051c3632a | ||
Paul Schaub | 0563105b1f | ||
Paul Schaub | 72ca392386 | ||
Paul Schaub | a5c332737b | ||
Paul Schaub | 41acdfe03a | ||
Paul Schaub | edef899074 | ||
Paul Schaub | baa44a6b1a | ||
Paul Schaub | 0c2cf5cb19 | ||
Paul Schaub | 5c2695228b | ||
Paul Schaub | b251956f49 | ||
Paul Schaub | b884f2b1a9 | ||
Paul Schaub | 2e118357e2 | ||
Paul Schaub | e9a5467f6b | ||
Paul Schaub | 019dd63e1b | ||
Paul Schaub | bfad8c4203 | ||
Paul Schaub | 159ffbe084 | ||
Paul Schaub | 714c933cef | ||
Paul Schaub | 9daabb758a | ||
Paul Schaub | 8e65771e36 | ||
Paul Schaub | 688b8043a2 | ||
Paul Schaub | 49120c5da8 | ||
Paul Schaub | 377a7287b3 | ||
Paul Schaub | 18865feaff | ||
Paul Schaub | 666d51384b | ||
Paul Schaub | 256d1c5960 | ||
Paul Schaub | 8246359a85 | ||
Paul Schaub | 1de179c015 | ||
Paul Schaub | 86b173bf1c | ||
Paul Schaub | 5ee9414410 | ||
Paul Schaub | a8829350a8 | ||
Paul Schaub | 7824ee92c5 | ||
Paul Schaub | 94b428ef62 | ||
Paul Schaub | e1a6ffd07a | ||
Paul Schaub | 25a33611fd | ||
Paul Schaub | 05886228df | ||
Paul Schaub | b7007cc007 | ||
Paul Schaub | 01f98df80b | ||
Paul Schaub | 30c369d24a | ||
Paul Schaub | be6be3deac | ||
Paul Schaub | 1c290e0c8f | ||
Paul Schaub | d5c0d4e390 | ||
Paul Schaub | 4b9e2c206f | ||
Paul Schaub | 049c18c17b | ||
Paul Schaub | d0ee9c2066 | ||
Paul Schaub | a8c2e72ef5 | ||
Paul Schaub | 0ee4638beb | ||
Paul Schaub | 145cadef4f | ||
Paul Schaub | 6c14f249bb | ||
Paul Schaub | be0ceb0886 | ||
Paul Schaub | 9283f81c56 | ||
Paul Schaub | 8df4a520bd | ||
Paul Schaub | 3e6ebe1cc4 | ||
Paul Schaub | 653675f730 | ||
Paul Schaub | 41db9d2ac7 | ||
Paul Schaub | e681090757 | ||
Paul Schaub | ee6975c7d3 | ||
Paul Schaub | 4dc1779a06 | ||
Paul Schaub | 91a861b5c3 | ||
Paul Schaub | 39c222dfc8 | ||
Paul Schaub | 34e1d8992f | ||
Paul Schaub | 4a123a1980 | ||
Paul Schaub | 08ddc5d8a5 | ||
Paul Schaub | e68d6df57f | ||
Paul Schaub | 31409b7949 | ||
Paul Schaub | dc23c8aa98 | ||
Paul Schaub | 2391ffc9b2 | ||
Paul Schaub | a89e70c19e | ||
Paul Schaub | e6562cecff | ||
Paul Schaub | 9dbb93e13d | ||
Paul Schaub | bbe159e88c | ||
Paul Schaub | 0cb5c74a11 | ||
Paul Schaub | ef4b01c6bd | ||
Paul Schaub | 6c5c4b3d98 | ||
Paul Schaub | 567571cf6c | ||
Paul Schaub | 0f5270c28d | ||
Paul Schaub | 4bd4657906 | ||
Paul Schaub | cf1d39643d |
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
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. -->
|
|
@ -22,3 +22,8 @@ License: Apache-2.0
|
||||||
Files: external-sop/src/main/resources/sop/testsuite/external/*
|
Files: external-sop/src/main/resources/sop/testsuite/external/*
|
||||||
Copyright: 2023 the original author or authors
|
Copyright: 2023 the original author or authors
|
||||||
License: Apache-2.0
|
License: Apache-2.0
|
||||||
|
|
||||||
|
# Github Issue Templates
|
||||||
|
Files: .github/ISSUE_TEMPLATE/*
|
||||||
|
Copyright: 2024 the original author or authors
|
||||||
|
License: Apache-2.0
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
pipeline:
|
steps:
|
||||||
run:
|
run:
|
||||||
image: gradle:7.5-jdk8-jammy
|
when:
|
||||||
|
event: push
|
||||||
|
image: gradle:7.6-jdk11-jammy
|
||||||
commands:
|
commands:
|
||||||
# Install Sequoia-SOP
|
# Install Sequoia-SOP
|
||||||
- apt update && apt install --yes sqop
|
- apt update && apt install --yes sqop
|
||||||
|
@ -14,4 +16,4 @@ pipeline:
|
||||||
- gradle check javadocAll
|
- gradle check javadocAll
|
||||||
# Code has coverage
|
# Code has coverage
|
||||||
- gradle jacocoRootReport coveralls
|
- gradle jacocoRootReport coveralls
|
||||||
secrets: [COVERALLS_REPO_TOKEN]
|
secrets: [coveralls_repo_token]
|
|
@ -1,7 +1,9 @@
|
||||||
# Code is licensed properly
|
# Code is licensed properly
|
||||||
# See https://reuse.software/
|
# See https://reuse.software/
|
||||||
pipeline:
|
steps:
|
||||||
reuse:
|
reuse:
|
||||||
|
when:
|
||||||
|
event: push
|
||||||
image: fsfe/reuse:latest
|
image: fsfe/reuse:latest
|
||||||
commands:
|
commands:
|
||||||
- reuse lint
|
- reuse lint
|
29
CHANGELOG.md
29
CHANGELOG.md
|
@ -6,6 +6,35 @@ SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 10.0.1-SNAPSHOT
|
||||||
|
- Remove `label()` option from `Armor` operation
|
||||||
|
- Fix exit code for 'Missing required option/parameter' error
|
||||||
|
- Fix `revoke-key`: Allow for multiple invocations of `--with-key-password` option
|
||||||
|
|
||||||
|
## 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
|
## 7.0.0
|
||||||
- Update implementation to [SOP Specification revision 07](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-07.html).
|
- Update implementation to [SOP Specification revision 07](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-07.html).
|
||||||
- Add support for new `revoke-key` subcommand
|
- Add support for new `revoke-key` subcommand
|
||||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: Apache-2.0
|
||||||
# SOP for Java
|
# SOP for Java
|
||||||
|
|
||||||
[![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java)
|
[![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java)
|
||||||
[![Spec Revision: 7](https://img.shields.io/badge/Spec%20Revision-7-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/07/)
|
[![Spec Revision: 10](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/10/)
|
||||||
[![Coverage Status](https://coveralls.io/repos/github/pgpainless/sop-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/sop-java?branch=main)
|
[![Coverage Status](https://coveralls.io/repos/github/pgpainless/sop-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/sop-java?branch=main)
|
||||||
[![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/sop-java)](https://api.reuse.software/info/github.com/pgpainless/sop-java)
|
[![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/sop-java)](https://api.reuse.software/info/github.com/pgpainless/sop-java)
|
||||||
|
|
||||||
|
@ -25,6 +25,8 @@ 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!)
|
||||||
|
|
17
build.gradle
17
build.gradle
|
@ -19,6 +19,8 @@ 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'
|
||||||
|
@ -29,6 +31,8 @@ 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')) {
|
||||||
|
@ -53,6 +57,12 @@ 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
|
||||||
|
@ -69,6 +79,13 @@ 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,454 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external;
|
|
||||||
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.SOP;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.external.operation.ArmorExternal;
|
|
||||||
import sop.external.operation.ChangeKeyPasswordExternal;
|
|
||||||
import sop.external.operation.DearmorExternal;
|
|
||||||
import sop.external.operation.DecryptExternal;
|
|
||||||
import sop.external.operation.DetachedSignExternal;
|
|
||||||
import sop.external.operation.DetachedVerifyExternal;
|
|
||||||
import sop.external.operation.EncryptExternal;
|
|
||||||
import sop.external.operation.ExtractCertExternal;
|
|
||||||
import sop.external.operation.GenerateKeyExternal;
|
|
||||||
import sop.external.operation.InlineDetachExternal;
|
|
||||||
import sop.external.operation.InlineSignExternal;
|
|
||||||
import sop.external.operation.InlineVerifyExternal;
|
|
||||||
import sop.external.operation.ListProfilesExternal;
|
|
||||||
import sop.external.operation.RevokeKeyExternal;
|
|
||||||
import sop.external.operation.VersionExternal;
|
|
||||||
import sop.operation.Armor;
|
|
||||||
import sop.operation.ChangeKeyPassword;
|
|
||||||
import sop.operation.Dearmor;
|
|
||||||
import sop.operation.Decrypt;
|
|
||||||
import sop.operation.DetachedSign;
|
|
||||||
import sop.operation.DetachedVerify;
|
|
||||||
import sop.operation.Encrypt;
|
|
||||||
import sop.operation.ExtractCert;
|
|
||||||
import sop.operation.GenerateKey;
|
|
||||||
import sop.operation.InlineDetach;
|
|
||||||
import sop.operation.InlineSign;
|
|
||||||
import sop.operation.InlineVerify;
|
|
||||||
import sop.operation.ListProfiles;
|
|
||||||
import sop.operation.RevokeKey;
|
|
||||||
import sop.operation.Version;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.attribute.FileAttribute;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the {@link SOP} API using an external SOP binary.
|
|
||||||
*/
|
|
||||||
public class ExternalSOP implements SOP {
|
|
||||||
|
|
||||||
private final String binaryName;
|
|
||||||
private final Properties properties;
|
|
||||||
private final TempDirProvider tempDirProvider;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate an {@link ExternalSOP} object for the given binary and pass it empty environment variables,
|
|
||||||
* as well as a default {@link TempDirProvider}.
|
|
||||||
*
|
|
||||||
* @param binaryName name / path of the SOP binary
|
|
||||||
*/
|
|
||||||
public ExternalSOP(@Nonnull String binaryName) {
|
|
||||||
this(binaryName, new Properties());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate an {@link ExternalSOP} object for the given binary, and pass it the given properties as
|
|
||||||
* environment variables, as well as a default {@link TempDirProvider}.
|
|
||||||
*
|
|
||||||
* @param binaryName name / path of the SOP binary
|
|
||||||
* @param properties environment variables
|
|
||||||
*/
|
|
||||||
public ExternalSOP(@Nonnull String binaryName, @Nonnull Properties properties) {
|
|
||||||
this(binaryName, properties, defaultTempDirProvider());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate an {@link ExternalSOP} object for the given binary and the given {@link TempDirProvider}
|
|
||||||
* using empty environment variables.
|
|
||||||
*
|
|
||||||
* @param binaryName name / path of the SOP binary
|
|
||||||
* @param tempDirProvider custom tempDirProvider
|
|
||||||
*/
|
|
||||||
public ExternalSOP(@Nonnull String binaryName, @Nonnull TempDirProvider tempDirProvider) {
|
|
||||||
this(binaryName, new Properties(), tempDirProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate an {@link ExternalSOP} object for the given binary using the given properties and
|
|
||||||
* custom {@link TempDirProvider}.
|
|
||||||
*
|
|
||||||
* @param binaryName name / path of the SOP binary
|
|
||||||
* @param properties environment variables
|
|
||||||
* @param tempDirProvider tempDirProvider
|
|
||||||
*/
|
|
||||||
public ExternalSOP(@Nonnull String binaryName, @Nonnull Properties properties, @Nonnull TempDirProvider tempDirProvider) {
|
|
||||||
this.binaryName = binaryName;
|
|
||||||
this.properties = properties;
|
|
||||||
this.tempDirProvider = tempDirProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Version version() {
|
|
||||||
return new VersionExternal(binaryName, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public GenerateKey generateKey() {
|
|
||||||
return new GenerateKeyExternal(binaryName, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ExtractCert extractCert() {
|
|
||||||
return new ExtractCertExternal(binaryName, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DetachedSign detachedSign() {
|
|
||||||
return new DetachedSignExternal(binaryName, properties, tempDirProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InlineSign inlineSign() {
|
|
||||||
return new InlineSignExternal(binaryName, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DetachedVerify detachedVerify() {
|
|
||||||
return new DetachedVerifyExternal(binaryName, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InlineVerify inlineVerify() {
|
|
||||||
return new InlineVerifyExternal(binaryName, properties, tempDirProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InlineDetach inlineDetach() {
|
|
||||||
return new InlineDetachExternal(binaryName, properties, tempDirProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Encrypt encrypt() {
|
|
||||||
return new EncryptExternal(binaryName, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Decrypt decrypt() {
|
|
||||||
return new DecryptExternal(binaryName, properties, tempDirProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Armor armor() {
|
|
||||||
return new ArmorExternal(binaryName, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ListProfiles listProfiles() {
|
|
||||||
return new ListProfilesExternal(binaryName, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RevokeKey revokeKey() {
|
|
||||||
return new RevokeKeyExternal(binaryName, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ChangeKeyPassword changeKeyPassword() {
|
|
||||||
return new ChangeKeyPasswordExternal(binaryName, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Dearmor dearmor() {
|
|
||||||
return new DearmorExternal(binaryName, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void finish(@Nonnull Process process) throws IOException {
|
|
||||||
try {
|
|
||||||
mapExitCodeOrException(process);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for the {@link Process} to finish and read out its exit code.
|
|
||||||
* If the exit code is {@value "0"}, this method just returns.
|
|
||||||
* Otherwise, the exit code gets mapped to a {@link SOPGPException} which then gets thrown.
|
|
||||||
* If the exit code does not match any of the known exit codes defined in the SOP specification,
|
|
||||||
* this method throws a {@link RuntimeException} instead.
|
|
||||||
*
|
|
||||||
* @param process process
|
|
||||||
* @throws InterruptedException if the thread is interrupted before the process could exit
|
|
||||||
* @throws IOException in case of an IO error
|
|
||||||
*/
|
|
||||||
private static void mapExitCodeOrException(@Nonnull Process process) throws InterruptedException, IOException {
|
|
||||||
// wait for process termination
|
|
||||||
int exitCode = process.waitFor();
|
|
||||||
|
|
||||||
if (exitCode == 0) {
|
|
||||||
// we're good, bye
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read error message
|
|
||||||
InputStream errIn = process.getErrorStream();
|
|
||||||
String errorMessage = readString(errIn);
|
|
||||||
|
|
||||||
switch (exitCode) {
|
|
||||||
case SOPGPException.NoSignature.EXIT_CODE:
|
|
||||||
throw new SOPGPException.NoSignature("External SOP backend reported error NoSignature (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE:
|
|
||||||
throw new UnsupportedOperationException("External SOP backend reported error UnsupportedAsymmetricAlgo (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.CertCannotEncrypt.EXIT_CODE:
|
|
||||||
throw new SOPGPException.CertCannotEncrypt("External SOP backend reported error CertCannotEncrypt (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.MissingArg.EXIT_CODE:
|
|
||||||
throw new SOPGPException.MissingArg("External SOP backend reported error MissingArg (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.IncompleteVerification.EXIT_CODE:
|
|
||||||
throw new SOPGPException.IncompleteVerification("External SOP backend reported error IncompleteVerification (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.CannotDecrypt.EXIT_CODE:
|
|
||||||
throw new SOPGPException.CannotDecrypt("External SOP backend reported error CannotDecrypt (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.PasswordNotHumanReadable.EXIT_CODE:
|
|
||||||
throw new SOPGPException.PasswordNotHumanReadable("External SOP backend reported error PasswordNotHumanReadable (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.UnsupportedOption.EXIT_CODE:
|
|
||||||
throw new SOPGPException.UnsupportedOption("External SOP backend reported error UnsupportedOption (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.BadData.EXIT_CODE:
|
|
||||||
throw new SOPGPException.BadData("External SOP backend reported error BadData (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.ExpectedText.EXIT_CODE:
|
|
||||||
throw new SOPGPException.ExpectedText("External SOP backend reported error ExpectedText (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.OutputExists.EXIT_CODE:
|
|
||||||
throw new SOPGPException.OutputExists("External SOP backend reported error OutputExists (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.MissingInput.EXIT_CODE:
|
|
||||||
throw new SOPGPException.MissingInput("External SOP backend reported error MissingInput (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.KeyIsProtected.EXIT_CODE:
|
|
||||||
throw new SOPGPException.KeyIsProtected("External SOP backend reported error KeyIsProtected (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.UnsupportedSubcommand.EXIT_CODE:
|
|
||||||
throw new SOPGPException.UnsupportedSubcommand("External SOP backend reported error UnsupportedSubcommand (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.UnsupportedSpecialPrefix.EXIT_CODE:
|
|
||||||
throw new SOPGPException.UnsupportedSpecialPrefix("External SOP backend reported error UnsupportedSpecialPrefix (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.AmbiguousInput.EXIT_CODE:
|
|
||||||
throw new SOPGPException.AmbiguousInput("External SOP backend reported error AmbiguousInput (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.KeyCannotSign.EXIT_CODE:
|
|
||||||
throw new SOPGPException.KeyCannotSign("External SOP backend reported error KeyCannotSign (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.IncompatibleOptions.EXIT_CODE:
|
|
||||||
throw new SOPGPException.IncompatibleOptions("External SOP backend reported error IncompatibleOptions (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
case SOPGPException.UnsupportedProfile.EXIT_CODE:
|
|
||||||
throw new SOPGPException.UnsupportedProfile("External SOP backend reported error UnsupportedProfile (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Did you forget to add a case for a new exception type?
|
|
||||||
throw new RuntimeException("External SOP backend reported unknown exit code (" +
|
|
||||||
exitCode + "):\n" + errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return all key-value pairs from the given {@link Properties} object as a list with items of the form
|
|
||||||
* <pre>key=value</pre>.
|
|
||||||
*
|
|
||||||
* @param properties properties
|
|
||||||
* @return list of key=value strings
|
|
||||||
*/
|
|
||||||
public static List<String> propertiesToEnv(@Nonnull Properties properties) {
|
|
||||||
List<String> env = new ArrayList<>();
|
|
||||||
for (Object key : properties.keySet()) {
|
|
||||||
env.add(key + "=" + properties.get(key));
|
|
||||||
}
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read the contents of the {@link InputStream} and return them as a {@link String}.
|
|
||||||
*
|
|
||||||
* @param inputStream input stream
|
|
||||||
* @return string
|
|
||||||
* @throws IOException in case of an IO error
|
|
||||||
*/
|
|
||||||
public static String readString(@Nonnull InputStream inputStream) throws IOException {
|
|
||||||
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
|
|
||||||
byte[] buf = new byte[4096];
|
|
||||||
int r;
|
|
||||||
while ((r = inputStream.read(buf)) > 0) {
|
|
||||||
bOut.write(buf, 0, r);
|
|
||||||
}
|
|
||||||
return bOut.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the given command on the given {@link Runtime} with the given list of environment variables.
|
|
||||||
* This command does not transform any input data, and instead is purely a producer.
|
|
||||||
*
|
|
||||||
* @param runtime runtime
|
|
||||||
* @param commandList command
|
|
||||||
* @param envList environment variables
|
|
||||||
* @return ready to read the result from
|
|
||||||
*/
|
|
||||||
public static Ready executeProducingOperation(@Nonnull Runtime runtime,
|
|
||||||
@Nonnull List<String> commandList,
|
|
||||||
@Nonnull List<String> envList) {
|
|
||||||
String[] command = commandList.toArray(new String[0]);
|
|
||||||
String[] env = envList.toArray(new String[0]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Process process = runtime.exec(command, env);
|
|
||||||
InputStream stdIn = process.getInputStream();
|
|
||||||
|
|
||||||
return new Ready() {
|
|
||||||
@Override
|
|
||||||
public void writeTo(OutputStream outputStream) throws IOException {
|
|
||||||
byte[] buf = new byte[4096];
|
|
||||||
int r;
|
|
||||||
while ((r = stdIn.read(buf)) >= 0) {
|
|
||||||
outputStream.write(buf, 0, r);
|
|
||||||
}
|
|
||||||
|
|
||||||
outputStream.flush();
|
|
||||||
outputStream.close();
|
|
||||||
|
|
||||||
ExternalSOP.finish(process);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the given command on the given runtime using the given environment variables.
|
|
||||||
* The given input stream provides input for the process.
|
|
||||||
* This command is a transformation, meaning it is given input data and transforms it into output data.
|
|
||||||
*
|
|
||||||
* @param runtime runtime
|
|
||||||
* @param commandList command
|
|
||||||
* @param envList environment variables
|
|
||||||
* @param standardIn stream of input data for the process
|
|
||||||
* @return ready to read the result from
|
|
||||||
*/
|
|
||||||
public static Ready executeTransformingOperation(@Nonnull Runtime runtime, @Nonnull List<String> commandList, @Nonnull List<String> envList, @Nonnull InputStream standardIn) {
|
|
||||||
String[] command = commandList.toArray(new String[0]);
|
|
||||||
String[] env = envList.toArray(new String[0]);
|
|
||||||
try {
|
|
||||||
Process process = runtime.exec(command, env);
|
|
||||||
OutputStream processOut = process.getOutputStream();
|
|
||||||
InputStream processIn = process.getInputStream();
|
|
||||||
|
|
||||||
return new Ready() {
|
|
||||||
@Override
|
|
||||||
public void writeTo(OutputStream outputStream) throws IOException {
|
|
||||||
byte[] buf = new byte[4096];
|
|
||||||
int r;
|
|
||||||
while ((r = standardIn.read(buf)) > 0) {
|
|
||||||
processOut.write(buf, 0, r);
|
|
||||||
}
|
|
||||||
standardIn.close();
|
|
||||||
|
|
||||||
try {
|
|
||||||
processOut.flush();
|
|
||||||
processOut.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Perhaps the stream is already closed, in which case we ignore the exception.
|
|
||||||
if (!"Stream closed".equals(e.getMessage())) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while ((r = processIn.read(buf)) > 0) {
|
|
||||||
outputStream.write(buf, 0 , r);
|
|
||||||
}
|
|
||||||
processIn.close();
|
|
||||||
|
|
||||||
outputStream.flush();
|
|
||||||
outputStream.close();
|
|
||||||
|
|
||||||
finish(process);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This interface can be used to provide a directory in which external SOP binaries can temporarily store
|
|
||||||
* additional results of OpenPGP operations such that the binding classes can parse them out from there.
|
|
||||||
* Unfortunately, on Java you cannot open {@link java.io.FileDescriptor FileDescriptors} arbitrarily, so we
|
|
||||||
* have to rely on temporary files to pass results.
|
|
||||||
* An example:
|
|
||||||
* <pre>sop decrypt</pre> can emit signature verifications via <pre>--verify-out=/path/to/tempfile</pre>.
|
|
||||||
* {@link DecryptExternal} will then parse the temp file to make the result available to consumers.
|
|
||||||
* Temporary files are deleted after being read, yet creating temp files for sensitive information on disk
|
|
||||||
* might pose a security risk. Use with care!
|
|
||||||
*/
|
|
||||||
public interface TempDirProvider {
|
|
||||||
File provideTempDirectory() throws IOException;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default implementation of the {@link TempDirProvider} which stores temporary files in the systems temp dir
|
|
||||||
* ({@link Files#createTempDirectory(String, FileAttribute[])}).
|
|
||||||
*
|
|
||||||
* @return default implementation
|
|
||||||
*/
|
|
||||||
public static TempDirProvider defaultTempDirProvider() {
|
|
||||||
return new TempDirProvider() {
|
|
||||||
@Override
|
|
||||||
public File provideTempDirectory() throws IOException {
|
|
||||||
return Files.createTempDirectory("ext-sop").toFile();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external.operation;
|
|
||||||
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.enums.ArmorLabel;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.external.ExternalSOP;
|
|
||||||
import sop.operation.Armor;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the {@link Armor} operation using an external SOP binary.
|
|
||||||
*/
|
|
||||||
public class ArmorExternal implements Armor {
|
|
||||||
|
|
||||||
private final List<String> commandList = new ArrayList<>();
|
|
||||||
private final List<String> envList;
|
|
||||||
|
|
||||||
public ArmorExternal(String binary, Properties environment) {
|
|
||||||
commandList.add(binary);
|
|
||||||
commandList.add("armor");
|
|
||||||
envList = ExternalSOP.propertiesToEnv(environment);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Armor label(ArmorLabel label) throws SOPGPException.UnsupportedOption {
|
|
||||||
commandList.add("--label=" + label);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Ready data(InputStream data) throws SOPGPException.BadData {
|
|
||||||
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external.operation;
|
|
||||||
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.external.ExternalSOP;
|
|
||||||
import sop.operation.ChangeKeyPassword;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
public class ChangeKeyPasswordExternal implements ChangeKeyPassword {
|
|
||||||
private final List<String> commandList = new ArrayList<>();
|
|
||||||
private final List<String> envList;
|
|
||||||
|
|
||||||
private int keyPasswordCounter = 0;
|
|
||||||
|
|
||||||
public ChangeKeyPasswordExternal(String binary, Properties environment) {
|
|
||||||
this.commandList.add(binary);
|
|
||||||
this.commandList.add("decrypt");
|
|
||||||
this.envList = ExternalSOP.propertiesToEnv(environment);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ChangeKeyPassword noArmor() {
|
|
||||||
this.commandList.add("--no-armor");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ChangeKeyPassword oldKeyPassphrase(String oldPassphrase) {
|
|
||||||
this.commandList.add("--old-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter);
|
|
||||||
this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + oldPassphrase);
|
|
||||||
keyPasswordCounter++;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ChangeKeyPassword newKeyPassphrase(String newPassphrase) {
|
|
||||||
this.commandList.add("--new-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter);
|
|
||||||
this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + newPassphrase);
|
|
||||||
keyPasswordCounter++;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Ready keys(InputStream inputStream) throws SOPGPException.KeyIsProtected, SOPGPException.BadData {
|
|
||||||
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, inputStream);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external.operation;
|
|
||||||
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.external.ExternalSOP;
|
|
||||||
import sop.operation.Dearmor;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the {@link Dearmor} operation using an external SOP binary.
|
|
||||||
*/
|
|
||||||
public class DearmorExternal implements Dearmor {
|
|
||||||
|
|
||||||
private final List<String> commandList = new ArrayList<>();
|
|
||||||
private final List<String> envList;
|
|
||||||
|
|
||||||
public DearmorExternal(String binary, Properties environment) {
|
|
||||||
commandList.add(binary);
|
|
||||||
commandList.add("dearmor");
|
|
||||||
envList = ExternalSOP.propertiesToEnv(environment);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Ready data(InputStream data) throws SOPGPException.BadData {
|
|
||||||
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,176 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external.operation;
|
|
||||||
|
|
||||||
import sop.DecryptionResult;
|
|
||||||
import sop.ReadyWithResult;
|
|
||||||
import sop.SessionKey;
|
|
||||||
import sop.Verification;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.external.ExternalSOP;
|
|
||||||
import sop.operation.Decrypt;
|
|
||||||
import sop.util.UTCUtil;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the {@link Decrypt} operation using an external SOP binary.
|
|
||||||
*/
|
|
||||||
public class DecryptExternal implements Decrypt {
|
|
||||||
|
|
||||||
private final ExternalSOP.TempDirProvider tempDirProvider;
|
|
||||||
private final List<String> commandList = new ArrayList<>();
|
|
||||||
private final List<String> envList;
|
|
||||||
|
|
||||||
private int verifyWithCounter = 0;
|
|
||||||
private int withSessionKeyCounter = 0;
|
|
||||||
private int withPasswordCounter = 0;
|
|
||||||
private int keyCounter = 0;
|
|
||||||
private int withKeyPasswordCounter = 0;
|
|
||||||
|
|
||||||
public DecryptExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) {
|
|
||||||
this.tempDirProvider = tempDirProvider;
|
|
||||||
this.commandList.add(binary);
|
|
||||||
this.commandList.add("decrypt");
|
|
||||||
this.envList = ExternalSOP.propertiesToEnv(environment);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Decrypt verifyNotBefore(Date timestamp)
|
|
||||||
throws SOPGPException.UnsupportedOption {
|
|
||||||
this.commandList.add("--verify-not-before=" + UTCUtil.formatUTCDate(timestamp));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Decrypt verifyNotAfter(Date timestamp)
|
|
||||||
throws SOPGPException.UnsupportedOption {
|
|
||||||
this.commandList.add("--verify-not-after=" + UTCUtil.formatUTCDate(timestamp));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Decrypt verifyWithCert(InputStream cert)
|
|
||||||
throws SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException {
|
|
||||||
String envVar = "VERIFY_WITH_" + verifyWithCounter++;
|
|
||||||
commandList.add("--verify-with=@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + ExternalSOP.readString(cert));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Decrypt withSessionKey(SessionKey sessionKey)
|
|
||||||
throws SOPGPException.UnsupportedOption {
|
|
||||||
String envVar = "SESSION_KEY_" + withSessionKeyCounter++;
|
|
||||||
commandList.add("--with-session-key=@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + sessionKey);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Decrypt withPassword(String password)
|
|
||||||
throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
|
|
||||||
String envVar = "PASSWORD_" + withPasswordCounter++;
|
|
||||||
commandList.add("--with-password=@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + password);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Decrypt withKey(InputStream key)
|
|
||||||
throws SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException {
|
|
||||||
String envVar = "KEY_" + keyCounter++;
|
|
||||||
commandList.add("@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + ExternalSOP.readString(key));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Decrypt withKeyPassword(byte[] password)
|
|
||||||
throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable {
|
|
||||||
String envVar = "KEY_PASSWORD_" + withKeyPasswordCounter++;
|
|
||||||
commandList.add("--with-key-password=@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + new String(password));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ReadyWithResult<DecryptionResult> ciphertext(InputStream ciphertext)
|
|
||||||
throws SOPGPException.BadData, SOPGPException.MissingArg, SOPGPException.CannotDecrypt,
|
|
||||||
SOPGPException.KeyIsProtected, IOException {
|
|
||||||
File tempDir = tempDirProvider.provideTempDirectory();
|
|
||||||
|
|
||||||
File sessionKeyOut = new File(tempDir, "session-key-out");
|
|
||||||
sessionKeyOut.delete();
|
|
||||||
commandList.add("--session-key-out=" + sessionKeyOut.getAbsolutePath());
|
|
||||||
|
|
||||||
File verifyOut = new File(tempDir, "verifications-out");
|
|
||||||
verifyOut.delete();
|
|
||||||
if (verifyWithCounter != 0) {
|
|
||||||
commandList.add("--verify-out=" + verifyOut.getAbsolutePath());
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] command = commandList.toArray(new String[0]);
|
|
||||||
String[] env = envList.toArray(new String[0]);
|
|
||||||
try {
|
|
||||||
Process process = Runtime.getRuntime().exec(command, env);
|
|
||||||
OutputStream processOut = process.getOutputStream();
|
|
||||||
InputStream processIn = process.getInputStream();
|
|
||||||
|
|
||||||
return new ReadyWithResult<DecryptionResult>() {
|
|
||||||
@Override
|
|
||||||
public DecryptionResult writeTo(OutputStream outputStream) throws IOException {
|
|
||||||
byte[] buf = new byte[4096];
|
|
||||||
int r;
|
|
||||||
while ((r = ciphertext.read(buf)) > 0) {
|
|
||||||
processOut.write(buf, 0, r);
|
|
||||||
}
|
|
||||||
|
|
||||||
ciphertext.close();
|
|
||||||
processOut.close();
|
|
||||||
|
|
||||||
while ((r = processIn.read(buf)) > 0) {
|
|
||||||
outputStream.write(buf, 0 , r);
|
|
||||||
}
|
|
||||||
|
|
||||||
processIn.close();
|
|
||||||
outputStream.close();
|
|
||||||
|
|
||||||
ExternalSOP.finish(process);
|
|
||||||
|
|
||||||
FileInputStream sessionKeyOutIn = new FileInputStream(sessionKeyOut);
|
|
||||||
String line = ExternalSOP.readString(sessionKeyOutIn);
|
|
||||||
SessionKey sessionKey = SessionKey.fromString(line.trim());
|
|
||||||
sessionKeyOutIn.close();
|
|
||||||
sessionKeyOut.delete();
|
|
||||||
|
|
||||||
List<Verification> verifications = new ArrayList<>();
|
|
||||||
if (verifyWithCounter != 0) {
|
|
||||||
FileInputStream verifyOutIn = new FileInputStream(verifyOut);
|
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(verifyOutIn));
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
verifications.add(Verification.fromString(line.trim()));
|
|
||||||
}
|
|
||||||
reader.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DecryptionResult(sessionKey, verifications);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external.operation;
|
|
||||||
|
|
||||||
import sop.MicAlg;
|
|
||||||
import sop.ReadyWithResult;
|
|
||||||
import sop.SigningResult;
|
|
||||||
import sop.enums.SignAs;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.external.ExternalSOP;
|
|
||||||
import sop.operation.DetachedSign;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the {@link DetachedSign} operation using an external SOP binary.
|
|
||||||
*/
|
|
||||||
public class DetachedSignExternal implements DetachedSign {
|
|
||||||
|
|
||||||
private final ExternalSOP.TempDirProvider tempDirProvider;
|
|
||||||
private final List<String> commandList = new ArrayList<>();
|
|
||||||
private final List<String> envList;
|
|
||||||
|
|
||||||
private int withKeyPasswordCounter = 0;
|
|
||||||
private int keyCounter = 0;
|
|
||||||
|
|
||||||
public DetachedSignExternal(String binary, Properties properties, ExternalSOP.TempDirProvider tempDirProvider) {
|
|
||||||
this.tempDirProvider = tempDirProvider;
|
|
||||||
commandList.add(binary);
|
|
||||||
commandList.add("sign");
|
|
||||||
envList = ExternalSOP.propertiesToEnv(properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DetachedSign noArmor() {
|
|
||||||
commandList.add("--no-armor");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DetachedSign key(InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException {
|
|
||||||
String envVar = "KEY_" + keyCounter++;
|
|
||||||
commandList.add("@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + ExternalSOP.readString(key));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DetachedSign withKeyPassword(byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable {
|
|
||||||
String envVar = "WITH_KEY_PASSWORD_" + withKeyPasswordCounter++;
|
|
||||||
commandList.add("--with-key-password=@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + new String(password));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DetachedSign mode(SignAs mode) throws SOPGPException.UnsupportedOption {
|
|
||||||
commandList.add("--as=" + mode);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ReadyWithResult<SigningResult> data(InputStream data)
|
|
||||||
throws IOException, SOPGPException.KeyIsProtected, SOPGPException.ExpectedText {
|
|
||||||
|
|
||||||
File tempDir = tempDirProvider.provideTempDirectory();
|
|
||||||
File micAlgOut = new File(tempDir, "micAlgOut");
|
|
||||||
micAlgOut.delete();
|
|
||||||
commandList.add("--micalg-out=" + micAlgOut.getAbsolutePath());
|
|
||||||
|
|
||||||
String[] command = commandList.toArray(new String[0]);
|
|
||||||
String[] env = envList.toArray(new String[0]);
|
|
||||||
try {
|
|
||||||
Process process = Runtime.getRuntime().exec(command, env);
|
|
||||||
OutputStream processOut = process.getOutputStream();
|
|
||||||
InputStream processIn = process.getInputStream();
|
|
||||||
|
|
||||||
return new ReadyWithResult<SigningResult>() {
|
|
||||||
@Override
|
|
||||||
public SigningResult writeTo(OutputStream outputStream) throws IOException {
|
|
||||||
byte[] buf = new byte[4096];
|
|
||||||
int r;
|
|
||||||
while ((r = data.read(buf)) > 0) {
|
|
||||||
processOut.write(buf, 0, r);
|
|
||||||
}
|
|
||||||
|
|
||||||
data.close();
|
|
||||||
try {
|
|
||||||
processOut.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Ignore Stream closed
|
|
||||||
if (!"Stream closed".equals(e.getMessage())) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while ((r = processIn.read(buf)) > 0) {
|
|
||||||
outputStream.write(buf, 0 , r);
|
|
||||||
}
|
|
||||||
|
|
||||||
processIn.close();
|
|
||||||
outputStream.close();
|
|
||||||
|
|
||||||
ExternalSOP.finish(process);
|
|
||||||
|
|
||||||
SigningResult.Builder builder = SigningResult.builder();
|
|
||||||
if (micAlgOut.exists()) {
|
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(micAlgOut)));
|
|
||||||
String line = reader.readLine();
|
|
||||||
if (line != null && !line.trim().isEmpty()) {
|
|
||||||
MicAlg micAlg = new MicAlg(line.trim());
|
|
||||||
builder.setMicAlg(micAlg);
|
|
||||||
}
|
|
||||||
reader.close();
|
|
||||||
micAlgOut.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external.operation;
|
|
||||||
|
|
||||||
import sop.Verification;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.external.ExternalSOP;
|
|
||||||
import sop.operation.DetachedVerify;
|
|
||||||
import sop.operation.VerifySignatures;
|
|
||||||
import sop.util.UTCUtil;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the {@link DetachedVerify} operation using an external SOP binary.
|
|
||||||
*/
|
|
||||||
public class DetachedVerifyExternal implements DetachedVerify {
|
|
||||||
|
|
||||||
private final List<String> commandList = new ArrayList<>();
|
|
||||||
private final List<String> envList;
|
|
||||||
|
|
||||||
private final Set<InputStream> certs = new HashSet<>();
|
|
||||||
private InputStream signatures;
|
|
||||||
private int certCounter = 0;
|
|
||||||
|
|
||||||
public DetachedVerifyExternal(String binary, Properties environment) {
|
|
||||||
commandList.add(binary);
|
|
||||||
commandList.add("verify");
|
|
||||||
envList = ExternalSOP.propertiesToEnv(environment);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DetachedVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption {
|
|
||||||
commandList.add("--not-before=" + UTCUtil.formatUTCDate(timestamp));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DetachedVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption {
|
|
||||||
commandList.add("--not-after=" + UTCUtil.formatUTCDate(timestamp));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DetachedVerify cert(InputStream cert) throws SOPGPException.BadData {
|
|
||||||
this.certs.add(cert);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public VerifySignatures signatures(InputStream signatures) throws SOPGPException.BadData {
|
|
||||||
this.signatures = signatures;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Verification> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData {
|
|
||||||
commandList.add("@ENV:SIGNATURE");
|
|
||||||
envList.add("SIGNATURE=" + ExternalSOP.readString(signatures));
|
|
||||||
|
|
||||||
for (InputStream cert : certs) {
|
|
||||||
String envVar = "CERT_" + certCounter++;
|
|
||||||
commandList.add("@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + ExternalSOP.readString(cert));
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] command = commandList.toArray(new String[0]);
|
|
||||||
String[] env = envList.toArray(new String[0]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Process process = Runtime.getRuntime().exec(command, env);
|
|
||||||
OutputStream processOut = process.getOutputStream();
|
|
||||||
InputStream processIn = process.getInputStream();
|
|
||||||
|
|
||||||
byte[] buf = new byte[4096];
|
|
||||||
int r;
|
|
||||||
while ((r = data.read(buf)) > 0) {
|
|
||||||
processOut.write(buf, 0, r);
|
|
||||||
}
|
|
||||||
|
|
||||||
data.close();
|
|
||||||
processOut.close();
|
|
||||||
|
|
||||||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(processIn));
|
|
||||||
List<Verification> verifications = new ArrayList<>();
|
|
||||||
|
|
||||||
String line = null;
|
|
||||||
while ((line = bufferedReader.readLine()) != null) {
|
|
||||||
verifications.add(Verification.fromString(line));
|
|
||||||
}
|
|
||||||
|
|
||||||
ExternalSOP.finish(process);
|
|
||||||
|
|
||||||
return verifications;
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external.operation;
|
|
||||||
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.enums.EncryptAs;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.external.ExternalSOP;
|
|
||||||
import sop.operation.Encrypt;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the {@link Encrypt} operation using an external SOP binary.
|
|
||||||
*/
|
|
||||||
public class EncryptExternal implements Encrypt {
|
|
||||||
|
|
||||||
private final List<String> commandList = new ArrayList<>();
|
|
||||||
private final List<String> envList;
|
|
||||||
private int SIGN_WITH_COUNTER = 0;
|
|
||||||
private int KEY_PASSWORD_COUNTER = 0;
|
|
||||||
private int PASSWORD_COUNTER = 0;
|
|
||||||
private int CERT_COUNTER = 0;
|
|
||||||
|
|
||||||
public EncryptExternal(String binary, Properties environment) {
|
|
||||||
this.commandList.add(binary);
|
|
||||||
this.commandList.add("encrypt");
|
|
||||||
this.envList = ExternalSOP.propertiesToEnv(environment);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Encrypt noArmor() {
|
|
||||||
this.commandList.add("--no-armor");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Encrypt mode(EncryptAs mode)
|
|
||||||
throws SOPGPException.UnsupportedOption {
|
|
||||||
this.commandList.add("--as=" + mode);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Encrypt signWith(InputStream key)
|
|
||||||
throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData,
|
|
||||||
IOException {
|
|
||||||
String envVar = "SIGN_WITH_" + SIGN_WITH_COUNTER++;
|
|
||||||
commandList.add("--sign-with=@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + ExternalSOP.readString(key));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Encrypt withKeyPassword(byte[] password)
|
|
||||||
throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
|
|
||||||
String envVar = "KEY_PASSWORD_" + KEY_PASSWORD_COUNTER++;
|
|
||||||
commandList.add("--with-key-password=@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + new String(password));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Encrypt withPassword(String password)
|
|
||||||
throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
|
|
||||||
String envVar = "PASSWORD_" + PASSWORD_COUNTER++;
|
|
||||||
commandList.add("--with-password=@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + password);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Encrypt withCert(InputStream cert)
|
|
||||||
throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData,
|
|
||||||
IOException {
|
|
||||||
String envVar = "CERT_" + CERT_COUNTER++;
|
|
||||||
commandList.add("@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + ExternalSOP.readString(cert));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Encrypt profile(String profileName) {
|
|
||||||
commandList.add("--profile=" + profileName);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Ready plaintext(InputStream plaintext)
|
|
||||||
throws SOPGPException.KeyIsProtected {
|
|
||||||
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, plaintext);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external.operation;
|
|
||||||
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.external.ExternalSOP;
|
|
||||||
import sop.operation.ExtractCert;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the {@link ExtractCert} operation using an external SOP binary.
|
|
||||||
*/
|
|
||||||
public class ExtractCertExternal implements ExtractCert {
|
|
||||||
|
|
||||||
private final List<String> commandList = new ArrayList<>();
|
|
||||||
private final List<String> envList;
|
|
||||||
|
|
||||||
public ExtractCertExternal(String binary, Properties properties) {
|
|
||||||
this.commandList.add(binary);
|
|
||||||
this.commandList.add("extract-cert");
|
|
||||||
this.envList = ExternalSOP.propertiesToEnv(properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ExtractCert noArmor() {
|
|
||||||
this.commandList.add("--no-armor");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Ready key(InputStream keyInputStream) throws SOPGPException.BadData {
|
|
||||||
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keyInputStream);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external.operation;
|
|
||||||
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.external.ExternalSOP;
|
|
||||||
import sop.operation.GenerateKey;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the {@link GenerateKey} operation using an external SOP binary.
|
|
||||||
*/
|
|
||||||
public class GenerateKeyExternal implements GenerateKey {
|
|
||||||
|
|
||||||
private final List<String> commandList = new ArrayList<>();
|
|
||||||
private final List<String> envList;
|
|
||||||
|
|
||||||
private int keyPasswordCounter = 0;
|
|
||||||
|
|
||||||
public GenerateKeyExternal(String binary, Properties environment) {
|
|
||||||
this.commandList.add(binary);
|
|
||||||
this.commandList.add("generate-key");
|
|
||||||
this.envList = ExternalSOP.propertiesToEnv(environment);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public GenerateKey noArmor() {
|
|
||||||
this.commandList.add("--no-armor");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public GenerateKey userId(String userId) {
|
|
||||||
this.commandList.add(userId);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public GenerateKey withKeyPassword(String password)
|
|
||||||
throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
|
|
||||||
this.commandList.add("--with-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter);
|
|
||||||
this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + password);
|
|
||||||
keyPasswordCounter++;
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public GenerateKey profile(String profile) {
|
|
||||||
commandList.add("--profile=" + profile);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public GenerateKey signingOnly() {
|
|
||||||
commandList.add("--signing-only");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Ready generate()
|
|
||||||
throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo {
|
|
||||||
return ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external.operation;
|
|
||||||
|
|
||||||
import sop.ReadyWithResult;
|
|
||||||
import sop.Signatures;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.external.ExternalSOP;
|
|
||||||
import sop.operation.InlineDetach;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the {@link InlineDetach} operation using an external SOP binary.
|
|
||||||
*/
|
|
||||||
public class InlineDetachExternal implements InlineDetach {
|
|
||||||
|
|
||||||
private final ExternalSOP.TempDirProvider tempDirProvider;
|
|
||||||
private final List<String> commandList = new ArrayList<>();
|
|
||||||
private final List<String> envList;
|
|
||||||
|
|
||||||
public InlineDetachExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) {
|
|
||||||
this.tempDirProvider = tempDirProvider;
|
|
||||||
commandList.add(binary);
|
|
||||||
commandList.add("inline-detach");
|
|
||||||
envList = ExternalSOP.propertiesToEnv(environment);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InlineDetach noArmor() {
|
|
||||||
commandList.add("--no-armor");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ReadyWithResult<Signatures> message(InputStream messageInputStream) throws IOException, SOPGPException.BadData {
|
|
||||||
File tempDir = tempDirProvider.provideTempDirectory();
|
|
||||||
|
|
||||||
File signaturesOut = new File(tempDir, "signatures");
|
|
||||||
signaturesOut.delete();
|
|
||||||
commandList.add("--signatures-out=" + signaturesOut.getAbsolutePath());
|
|
||||||
|
|
||||||
String[] command = commandList.toArray(new String[0]);
|
|
||||||
String[] env = envList.toArray(new String[0]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Process process = Runtime.getRuntime().exec(command, env);
|
|
||||||
OutputStream processOut = process.getOutputStream();
|
|
||||||
InputStream processIn = process.getInputStream();
|
|
||||||
|
|
||||||
return new ReadyWithResult<Signatures>() {
|
|
||||||
@Override
|
|
||||||
public Signatures writeTo(OutputStream outputStream) throws IOException {
|
|
||||||
byte[] buf = new byte[4096];
|
|
||||||
int r;
|
|
||||||
while ((r = messageInputStream.read(buf)) > 0) {
|
|
||||||
processOut.write(buf, 0, r);
|
|
||||||
}
|
|
||||||
|
|
||||||
messageInputStream.close();
|
|
||||||
processOut.close();
|
|
||||||
|
|
||||||
while ((r = processIn.read(buf)) > 0) {
|
|
||||||
outputStream.write(buf, 0 , r);
|
|
||||||
}
|
|
||||||
|
|
||||||
processIn.close();
|
|
||||||
outputStream.close();
|
|
||||||
|
|
||||||
ExternalSOP.finish(process);
|
|
||||||
|
|
||||||
FileInputStream signaturesOutIn = new FileInputStream(signaturesOut);
|
|
||||||
ByteArrayOutputStream signaturesBuffer = new ByteArrayOutputStream();
|
|
||||||
while ((r = signaturesOutIn.read(buf)) > 0) {
|
|
||||||
signaturesBuffer.write(buf, 0, r);
|
|
||||||
}
|
|
||||||
signaturesOutIn.close();
|
|
||||||
signaturesOut.delete();
|
|
||||||
|
|
||||||
final byte[] sigBytes = signaturesBuffer.toByteArray();
|
|
||||||
return new Signatures() {
|
|
||||||
@Override
|
|
||||||
public void writeTo(OutputStream signatureOutputStream) throws IOException {
|
|
||||||
signatureOutputStream.write(sigBytes);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external.operation;
|
|
||||||
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.enums.InlineSignAs;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.external.ExternalSOP;
|
|
||||||
import sop.operation.InlineSign;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the {@link InlineSign} operation using an external SOP binary.
|
|
||||||
*/
|
|
||||||
public class InlineSignExternal implements InlineSign {
|
|
||||||
|
|
||||||
private final List<String> commandList = new ArrayList<>();
|
|
||||||
private final List<String> envList;
|
|
||||||
|
|
||||||
private int keyCounter = 0;
|
|
||||||
private int withKeyPasswordCounter = 0;
|
|
||||||
|
|
||||||
public InlineSignExternal(String binary, Properties environment) {
|
|
||||||
commandList.add(binary);
|
|
||||||
commandList.add("inline-sign");
|
|
||||||
envList = ExternalSOP.propertiesToEnv(environment);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InlineSign noArmor() {
|
|
||||||
commandList.add("--no-armor");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InlineSign key(InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException {
|
|
||||||
String envVar = "KEY_" + keyCounter++;
|
|
||||||
commandList.add("@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + ExternalSOP.readString(key));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InlineSign withKeyPassword(byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable {
|
|
||||||
String envVar = "WITH_KEY_PASSWORD_" + withKeyPasswordCounter++;
|
|
||||||
commandList.add("--with-key-password=@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + new String(password));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InlineSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption {
|
|
||||||
commandList.add("--as=" + mode);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText {
|
|
||||||
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,117 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external.operation;
|
|
||||||
|
|
||||||
import sop.ReadyWithResult;
|
|
||||||
import sop.Verification;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.external.ExternalSOP;
|
|
||||||
import sop.operation.InlineVerify;
|
|
||||||
import sop.util.UTCUtil;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the {@link InlineVerify} operation using an external SOP binary.
|
|
||||||
*/
|
|
||||||
public class InlineVerifyExternal implements InlineVerify {
|
|
||||||
|
|
||||||
private final ExternalSOP.TempDirProvider tempDirProvider;
|
|
||||||
private final List<String> commandList = new ArrayList<>();
|
|
||||||
private final List<String> envList;
|
|
||||||
|
|
||||||
private int certCounter = 0;
|
|
||||||
|
|
||||||
public InlineVerifyExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) {
|
|
||||||
this.tempDirProvider = tempDirProvider;
|
|
||||||
commandList.add(binary);
|
|
||||||
commandList.add("inline-verify");
|
|
||||||
envList = ExternalSOP.propertiesToEnv(environment);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InlineVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption {
|
|
||||||
commandList.add("--not-before=" + UTCUtil.formatUTCDate(timestamp));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InlineVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption {
|
|
||||||
commandList.add("--not-after=" + UTCUtil.formatUTCDate(timestamp));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InlineVerify cert(InputStream cert) throws SOPGPException.BadData, IOException {
|
|
||||||
String envVar = "CERT_" + certCounter++;
|
|
||||||
commandList.add("@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + ExternalSOP.readString(cert));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ReadyWithResult<List<Verification>> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData {
|
|
||||||
File tempDir = tempDirProvider.provideTempDirectory();
|
|
||||||
|
|
||||||
File verificationsOut = new File(tempDir, "verifications-out");
|
|
||||||
verificationsOut.delete();
|
|
||||||
commandList.add("--verifications-out=" + verificationsOut.getAbsolutePath());
|
|
||||||
|
|
||||||
String[] command = commandList.toArray(new String[0]);
|
|
||||||
String[] env = envList.toArray(new String[0]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Process process = Runtime.getRuntime().exec(command, env);
|
|
||||||
OutputStream processOut = process.getOutputStream();
|
|
||||||
InputStream processIn = process.getInputStream();
|
|
||||||
|
|
||||||
return new ReadyWithResult<List<Verification>>() {
|
|
||||||
@Override
|
|
||||||
public List<Verification> writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
|
|
||||||
byte[] buf = new byte[4096];
|
|
||||||
int r;
|
|
||||||
while ((r = data.read(buf)) > 0) {
|
|
||||||
processOut.write(buf, 0, r);
|
|
||||||
}
|
|
||||||
|
|
||||||
data.close();
|
|
||||||
processOut.close();
|
|
||||||
|
|
||||||
|
|
||||||
while ((r = processIn.read(buf)) > 0) {
|
|
||||||
outputStream.write(buf, 0 , r);
|
|
||||||
}
|
|
||||||
|
|
||||||
processIn.close();
|
|
||||||
outputStream.close();
|
|
||||||
|
|
||||||
ExternalSOP.finish(process);
|
|
||||||
|
|
||||||
FileInputStream verificationsOutIn = new FileInputStream(verificationsOut);
|
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(verificationsOutIn));
|
|
||||||
List<Verification> verificationList = new ArrayList<>();
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
verificationList.add(Verification.fromString(line.trim()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return verificationList;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external.operation;
|
|
||||||
|
|
||||||
import sop.Profile;
|
|
||||||
import sop.external.ExternalSOP;
|
|
||||||
import sop.operation.ListProfiles;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
public class ListProfilesExternal implements ListProfiles {
|
|
||||||
|
|
||||||
private final List<String> commandList = new ArrayList<>();
|
|
||||||
private final List<String> envList;
|
|
||||||
|
|
||||||
public ListProfilesExternal(String binary, Properties properties) {
|
|
||||||
this.commandList.add(binary);
|
|
||||||
this.commandList.add("list-profiles");
|
|
||||||
this.envList = ExternalSOP.propertiesToEnv(properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Profile> subcommand(String command) {
|
|
||||||
commandList.add(command);
|
|
||||||
try {
|
|
||||||
String output = new String(ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList).getBytes());
|
|
||||||
return toProfiles(output);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Profile> toProfiles(String output) {
|
|
||||||
List<Profile> profiles = new ArrayList<>();
|
|
||||||
for (String line : output.split("\n")) {
|
|
||||||
if (line.trim().isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
profiles.add(Profile.parse(line));
|
|
||||||
}
|
|
||||||
return profiles;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external.operation;
|
|
||||||
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.external.ExternalSOP;
|
|
||||||
import sop.operation.RevokeKey;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
public class RevokeKeyExternal implements RevokeKey {
|
|
||||||
|
|
||||||
private final List<String> commandList = new ArrayList<>();
|
|
||||||
private final List<String> envList;
|
|
||||||
|
|
||||||
private int withKeyPasswordCounter = 0;
|
|
||||||
|
|
||||||
public RevokeKeyExternal(String binary, Properties environment) {
|
|
||||||
this.commandList.add(binary);
|
|
||||||
this.commandList.add("revoke-key");
|
|
||||||
this.envList = ExternalSOP.propertiesToEnv(environment);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RevokeKey noArmor() {
|
|
||||||
this.commandList.add("--no-armor");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RevokeKey withKeyPassword(byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable {
|
|
||||||
String envVar = "KEY_PASSWORD_" + withKeyPasswordCounter++;
|
|
||||||
commandList.add("--with-key-password=@ENV:" + envVar);
|
|
||||||
envList.add(envVar + "=" + new String(password));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Ready keys(InputStream keys) {
|
|
||||||
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,157 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.external.operation;
|
|
||||||
|
|
||||||
import sop.external.ExternalSOP;
|
|
||||||
import sop.operation.Version;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the {@link Version} operation using an external SOP binary.
|
|
||||||
*/
|
|
||||||
public class VersionExternal implements Version {
|
|
||||||
|
|
||||||
private final Runtime runtime = Runtime.getRuntime();
|
|
||||||
private final String binary;
|
|
||||||
private final Properties environment;
|
|
||||||
|
|
||||||
public VersionExternal(String binaryName, Properties environment) {
|
|
||||||
this.binary = binaryName;
|
|
||||||
this.environment = environment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
String[] command = new String[] {binary, "version"};
|
|
||||||
String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]);
|
|
||||||
try {
|
|
||||||
Process process = runtime.exec(command, env);
|
|
||||||
BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
|
||||||
String line = stdInput.readLine().trim();
|
|
||||||
ExternalSOP.finish(process);
|
|
||||||
if (line.contains(" ")) {
|
|
||||||
return line.substring(0, line.lastIndexOf(" "));
|
|
||||||
}
|
|
||||||
return line;
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getVersion() {
|
|
||||||
String[] command = new String[] {binary, "version"};
|
|
||||||
String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]);
|
|
||||||
try {
|
|
||||||
Process process = runtime.exec(command, env);
|
|
||||||
BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
|
||||||
String line = stdInput.readLine().trim();
|
|
||||||
ExternalSOP.finish(process);
|
|
||||||
if (line.contains(" ")) {
|
|
||||||
return line.substring(line.lastIndexOf(" ") + 1);
|
|
||||||
}
|
|
||||||
return line;
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getBackendVersion() {
|
|
||||||
String[] command = new String[] {binary, "version", "--backend"};
|
|
||||||
String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]);
|
|
||||||
try {
|
|
||||||
Process process = runtime.exec(command, env);
|
|
||||||
BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = stdInput.readLine()) != null) {
|
|
||||||
sb.append(line).append('\n');
|
|
||||||
}
|
|
||||||
ExternalSOP.finish(process);
|
|
||||||
return sb.toString();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getExtendedVersion() {
|
|
||||||
String[] command = new String[] {binary, "version", "--extended"};
|
|
||||||
String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]);
|
|
||||||
try {
|
|
||||||
Process process = runtime.exec(command, env);
|
|
||||||
BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = stdInput.readLine()) != null) {
|
|
||||||
sb.append(line).append('\n');
|
|
||||||
}
|
|
||||||
ExternalSOP.finish(process);
|
|
||||||
return sb.toString();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getSopSpecRevisionNumber() {
|
|
||||||
String revision = getSopSpecVersion();
|
|
||||||
String firstLine;
|
|
||||||
if (revision.contains("\n")) {
|
|
||||||
firstLine = revision.substring(0, revision.indexOf("\n"));
|
|
||||||
} else {
|
|
||||||
firstLine = revision;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!firstLine.contains("-")) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Integer.parseInt(firstLine.substring(firstLine.lastIndexOf("-") + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isSopSpecImplementationIncomplete() {
|
|
||||||
String revision = getSopSpecVersion();
|
|
||||||
return revision.startsWith("~");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getSopSpecImplementationRemarks() {
|
|
||||||
String revision = getSopSpecVersion();
|
|
||||||
if (revision.contains("\n")) {
|
|
||||||
String tail = revision.substring(revision.indexOf("\n") + 1).trim();
|
|
||||||
|
|
||||||
if (!tail.isEmpty()) {
|
|
||||||
return tail;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getSopSpecVersion() {
|
|
||||||
String[] command = new String[] {binary, "version", "--sop-spec"};
|
|
||||||
String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]);
|
|
||||||
try {
|
|
||||||
Process process = runtime.exec(command, env);
|
|
||||||
BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = stdInput.readLine()) != null) {
|
|
||||||
sb.append(line).append('\n');
|
|
||||||
}
|
|
||||||
ExternalSOP.finish(process);
|
|
||||||
return sb.toString();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of sop-java which delegates execution to a binary implementing the SOP command line interface.
|
|
||||||
*/
|
|
||||||
package sop.external;
|
|
318
external-sop/src/main/kotlin/sop/external/ExternalSOP.kt
vendored
Normal file
318
external-sop/src/main/kotlin/sop/external/ExternalSOP.kt
vendored
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
// 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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt
vendored
Normal file
53
external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt
vendored
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// 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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt
vendored
Normal file
23
external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// 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)
|
||||||
|
}
|
37
external-sop/src/main/kotlin/sop/external/operation/ChangeKeyPasswordExternal.kt
vendored
Normal file
37
external-sop/src/main/kotlin/sop/external/operation/ChangeKeyPasswordExternal.kt
vendored
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// 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)
|
||||||
|
}
|
20
external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt
vendored
Normal file
20
external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// 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)
|
||||||
|
}
|
133
external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt
vendored
Normal file
133
external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt
vendored
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
// 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: String? = readString(sessionKeyOutIn)
|
||||||
|
val sessionKey = line?.let { l -> SessionKey.fromString(l.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) {
|
||||||
|
line?.let { verifications.add(Verification.fromString(it.trim())) }
|
||||||
|
}
|
||||||
|
reader.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return DecryptionResult(sessionKey, verifications)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
104
external-sop/src/main/kotlin/sop/external/operation/DetachedSignExternal.kt
vendored
Normal file
104
external-sop/src/main/kotlin/sop/external/operation/DetachedSignExternal.kt
vendored
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
90
external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt
vendored
Normal file
90
external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt
vendored
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
111
external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt
vendored
Normal file
111
external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt
vendored
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
external-sop/src/main/kotlin/sop/external/operation/ExtractCertExternal.kt
vendored
Normal file
24
external-sop/src/main/kotlin/sop/external/operation/ExtractCertExternal.kt
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// 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)
|
||||||
|
}
|
38
external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt
vendored
Normal file
38
external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt
vendored
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// 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)
|
||||||
|
}
|
82
external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt
vendored
Normal file
82
external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt
vendored
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt
vendored
Normal file
40
external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt
vendored
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
91
external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt
vendored
Normal file
91
external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt
vendored
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
36
external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt
vendored
Normal file
36
external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt
vendored
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// 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) }
|
||||||
|
}
|
||||||
|
}
|
31
external-sop/src/main/kotlin/sop/external/operation/RevokeKeyExternal.kt
vendored
Normal file
31
external-sop/src/main/kotlin/sop/external/operation/RevokeKeyExternal.kt
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// 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)
|
||||||
|
}
|
102
external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt
vendored
Normal file
102
external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt
vendored
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,15 +12,12 @@ 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"
|
||||||
|
@ -38,6 +35,7 @@ application {
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
dependsOn(":sop-java:jar")
|
dependsOn(":sop-java:jar")
|
||||||
|
duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
|
||||||
|
|
||||||
manifest {
|
manifest {
|
||||||
attributes 'Main-Class': "$mainClassName"
|
attributes 'Main-Class': "$mainClassName"
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.cli.picocli;
|
|
||||||
|
|
||||||
public class Print {
|
|
||||||
|
|
||||||
public static void outln(String string) {
|
|
||||||
// CHECKSTYLE:OFF
|
|
||||||
System.out.println(string);
|
|
||||||
// CHECKSTYLE:ON
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
// 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()));
|
|
||||||
} else {
|
|
||||||
commandLine.getErr().println(ex.getClass().getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SopCLI.stacktrace) {
|
|
||||||
ex.printStackTrace(commandLine.getErr());
|
|
||||||
}
|
|
||||||
// CHECKSTYLE:ON
|
|
||||||
|
|
||||||
return exitCode;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,129 +0,0 @@
|
||||||
// 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.ChangeKeyPasswordCmd;
|
|
||||||
import sop.cli.picocli.commands.DearmorCmd;
|
|
||||||
import sop.cli.picocli.commands.DecryptCmd;
|
|
||||||
import sop.cli.picocli.commands.InlineDetachCmd;
|
|
||||||
import sop.cli.picocli.commands.EncryptCmd;
|
|
||||||
import sop.cli.picocli.commands.ExtractCertCmd;
|
|
||||||
import sop.cli.picocli.commands.GenerateKeyCmd;
|
|
||||||
import sop.cli.picocli.commands.InlineSignCmd;
|
|
||||||
import sop.cli.picocli.commands.InlineVerifyCmd;
|
|
||||||
import sop.cli.picocli.commands.ListProfilesCmd;
|
|
||||||
import sop.cli.picocli.commands.RevokeKeyCmd;
|
|
||||||
import sop.cli.picocli.commands.SignCmd;
|
|
||||||
import sop.cli.picocli.commands.VerifyCmd;
|
|
||||||
import sop.cli.picocli.commands.VersionCmd;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.ResourceBundle;
|
|
||||||
|
|
||||||
@CommandLine.Command(
|
|
||||||
name = "sop",
|
|
||||||
resourceBundle = "msg_sop",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedSubcommand.EXIT_CODE,
|
|
||||||
subcommands = {
|
|
||||||
// Meta Subcommands
|
|
||||||
VersionCmd.class,
|
|
||||||
ListProfilesCmd.class,
|
|
||||||
// Key and Certificate Management Subcommands
|
|
||||||
GenerateKeyCmd.class,
|
|
||||||
ChangeKeyPasswordCmd.class,
|
|
||||||
RevokeKeyCmd.class,
|
|
||||||
ExtractCertCmd.class,
|
|
||||||
// Messaging Subcommands
|
|
||||||
SignCmd.class,
|
|
||||||
VerifyCmd.class,
|
|
||||||
EncryptCmd.class,
|
|
||||||
DecryptCmd.class,
|
|
||||||
InlineDetachCmd.class,
|
|
||||||
InlineSignCmd.class,
|
|
||||||
InlineVerifyCmd.class,
|
|
||||||
// Transport Subcommands
|
|
||||||
ArmorCmd.class,
|
|
||||||
DearmorCmd.class,
|
|
||||||
// Miscellaneous Subcommands
|
|
||||||
CommandLine.HelpCommand.class,
|
|
||||||
AutoComplete.GenerateCompletion.class
|
|
||||||
}
|
|
||||||
)
|
|
||||||
public class SopCLI {
|
|
||||||
// Singleton
|
|
||||||
static SOP SOP_INSTANCE;
|
|
||||||
static ResourceBundle cliMsg = ResourceBundle.getBundle("msg_sop");
|
|
||||||
|
|
||||||
public static String EXECUTABLE_NAME = "sop";
|
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--stacktrace"},
|
|
||||||
scope = CommandLine.ScopeType.INHERIT)
|
|
||||||
static boolean stacktrace;
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
int exitCode = execute(args);
|
|
||||||
if (exitCode != 0) {
|
|
||||||
System.exit(exitCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int execute(String[] args) {
|
|
||||||
|
|
||||||
// Set locale
|
|
||||||
new CommandLine(new InitLocale()).parseArgs(args);
|
|
||||||
|
|
||||||
// get error message bundle
|
|
||||||
cliMsg = ResourceBundle.getBundle("msg_sop");
|
|
||||||
|
|
||||||
// Prepare CLI
|
|
||||||
CommandLine cmd = new CommandLine(SopCLI.class);
|
|
||||||
|
|
||||||
// explicitly set help command resource bundle
|
|
||||||
cmd.getSubcommands().get("help").setResourceBundle(ResourceBundle.getBundle("msg_help"));
|
|
||||||
|
|
||||||
// Hide generate-completion command
|
|
||||||
cmd.getSubcommands().get("generate-completion").getCommandSpec().usageMessage().hidden(true);
|
|
||||||
|
|
||||||
cmd.setCommandName(EXECUTABLE_NAME)
|
|
||||||
.setExecutionExceptionHandler(new SOPExecutionExceptionHandler())
|
|
||||||
.setExitCodeExceptionMapper(new SOPExceptionExitCodeMapper())
|
|
||||||
.setCaseInsensitiveEnumValuesAllowed(true);
|
|
||||||
|
|
||||||
return cmd.execute(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SOP getSop() {
|
|
||||||
if (SOP_INSTANCE == null) {
|
|
||||||
String errorMsg = cliMsg.getString("sop.error.runtime.no_backend_set");
|
|
||||||
throw new IllegalStateException(errorMsg);
|
|
||||||
}
|
|
||||||
return SOP_INSTANCE;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setSopInstance(SOP instance) {
|
|
||||||
SOP_INSTANCE = instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Control the locale.
|
|
||||||
*
|
|
||||||
* @see <a href="https://picocli.info/#_controlling_the_locale">Picocli Readme</a>
|
|
||||||
*/
|
|
||||||
class InitLocale {
|
|
||||||
@CommandLine.Option(names = { "-l", "--locale" }, descriptionKey = "sop.locale")
|
|
||||||
void setLocale(String locale) {
|
|
||||||
Locale.setDefault(new Locale(locale));
|
|
||||||
}
|
|
||||||
|
|
||||||
@CommandLine.Unmatched
|
|
||||||
List<String> remainder; // ignore any other parameters and options in the first parsing phase
|
|
||||||
}
|
|
|
@ -1,282 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
|
||||||
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.util.UTCUtil;
|
|
||||||
import sop.util.UTF8Util;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.ResourceBundle;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract super class of SOP subcommands.
|
|
||||||
*/
|
|
||||||
public abstract class AbstractSopCmd implements Runnable {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface to modularize resolving of environment variables.
|
|
||||||
*/
|
|
||||||
public interface EnvironmentVariableResolver {
|
|
||||||
/**
|
|
||||||
* Resolve the value of the given environment variable.
|
|
||||||
* Return null if the variable is not present.
|
|
||||||
*
|
|
||||||
* @param name name of the variable
|
|
||||||
* @return variable value or null
|
|
||||||
*/
|
|
||||||
String resolveEnvironmentVariable(String name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final String PRFX_ENV = "@ENV:";
|
|
||||||
public static final String PRFX_FD = "@FD:";
|
|
||||||
public static final Date BEGINNING_OF_TIME = new Date(0);
|
|
||||||
public static final Date END_OF_TIME = new Date(8640000000000000L);
|
|
||||||
|
|
||||||
public static final Pattern PATTERN_FD = Pattern.compile("^\\d{1,20}$");
|
|
||||||
|
|
||||||
protected final ResourceBundle messages;
|
|
||||||
protected EnvironmentVariableResolver envResolver = System::getenv;
|
|
||||||
|
|
||||||
public AbstractSopCmd() {
|
|
||||||
this(Locale.getDefault());
|
|
||||||
}
|
|
||||||
|
|
||||||
public AbstractSopCmd(@Nonnull Locale locale) {
|
|
||||||
messages = ResourceBundle.getBundle("msg_sop", locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
void throwIfOutputExists(String output) {
|
|
||||||
if (output == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
File outputFile = new File(output);
|
|
||||||
if (outputFile.exists()) {
|
|
||||||
String errorMsg = getMsg("sop.error.indirect_data_type.output_file_already_exists", outputFile.getAbsolutePath());
|
|
||||||
throw new SOPGPException.OutputExists(errorMsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMsg(String key) {
|
|
||||||
return messages.getString(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMsg(String key, String arg1) {
|
|
||||||
return String.format(messages.getString(key), arg1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMsg(String key, String arg1, String arg2) {
|
|
||||||
return String.format(messages.getString(key), arg1, arg2);
|
|
||||||
}
|
|
||||||
|
|
||||||
void throwIfMissingArg(Object arg, String argName) {
|
|
||||||
if (arg == null) {
|
|
||||||
String errorMsg = getMsg("sop.error.usage.argument_required", argName);
|
|
||||||
throw new SOPGPException.MissingArg(errorMsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void throwIfEmptyParameters(Collection<?> arg, String parmName) {
|
|
||||||
if (arg.isEmpty()) {
|
|
||||||
String errorMsg = getMsg("sop.error.usage.parameter_required", parmName);
|
|
||||||
throw new SOPGPException.MissingArg(errorMsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<T> T throwIfUnsupportedSubcommand(T subcommand, String subcommandName) {
|
|
||||||
if (subcommand == null) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.subcommand_not_supported", subcommandName);
|
|
||||||
throw new SOPGPException.UnsupportedSubcommand(errorMsg);
|
|
||||||
}
|
|
||||||
return subcommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setEnvironmentVariableResolver(EnvironmentVariableResolver envResolver) {
|
|
||||||
if (envResolver == null) {
|
|
||||||
throw new NullPointerException("Variable envResolver cannot be null.");
|
|
||||||
}
|
|
||||||
this.envResolver = envResolver;
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream getInput(String indirectInput) throws IOException {
|
|
||||||
if (indirectInput == null) {
|
|
||||||
throw new IllegalArgumentException("Input cannot not be null.");
|
|
||||||
}
|
|
||||||
|
|
||||||
String trimmed = indirectInput.trim();
|
|
||||||
if (trimmed.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("Input cannot be blank.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trimmed.startsWith(PRFX_ENV)) {
|
|
||||||
if (new File(trimmed).exists()) {
|
|
||||||
String errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed);
|
|
||||||
throw new SOPGPException.AmbiguousInput(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
String envName = trimmed.substring(PRFX_ENV.length());
|
|
||||||
String envValue = envResolver.resolveEnvironmentVariable(envName);
|
|
||||||
if (envValue == null) {
|
|
||||||
String errorMsg = getMsg("sop.error.indirect_data_type.environment_variable_not_set", envName);
|
|
||||||
throw new IllegalArgumentException(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (envValue.trim().isEmpty()) {
|
|
||||||
String errorMsg = getMsg("sop.error.indirect_data_type.environment_variable_empty", envName);
|
|
||||||
throw new IllegalArgumentException(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ByteArrayInputStream(envValue.getBytes("UTF8"));
|
|
||||||
|
|
||||||
} else if (trimmed.startsWith(PRFX_FD)) {
|
|
||||||
|
|
||||||
if (new File(trimmed).exists()) {
|
|
||||||
String errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed);
|
|
||||||
throw new SOPGPException.AmbiguousInput(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
File fdFile = fileDescriptorFromString(trimmed);
|
|
||||||
try {
|
|
||||||
FileInputStream fileIn = new FileInputStream(fdFile);
|
|
||||||
return fileIn;
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
String errorMsg = getMsg("sop.error.indirect_data_type.file_descriptor_not_found", fdFile.getAbsolutePath());
|
|
||||||
throw new IOException(errorMsg, e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
File file = new File(trimmed);
|
|
||||||
if (!file.exists()) {
|
|
||||||
String errorMsg = getMsg("sop.error.indirect_data_type.input_file_does_not_exist", file.getAbsolutePath());
|
|
||||||
throw new SOPGPException.MissingInput(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.isFile()) {
|
|
||||||
String errorMsg = getMsg("sop.error.indirect_data_type.input_not_a_file", file.getAbsolutePath());
|
|
||||||
throw new SOPGPException.MissingInput(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new FileInputStream(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public OutputStream getOutput(String indirectOutput) throws IOException {
|
|
||||||
if (indirectOutput == null) {
|
|
||||||
throw new IllegalArgumentException("Output cannot be null.");
|
|
||||||
}
|
|
||||||
|
|
||||||
String trimmed = indirectOutput.trim();
|
|
||||||
if (trimmed.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("Output cannot be blank.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ENV not allowed for output
|
|
||||||
if (trimmed.startsWith(PRFX_ENV)) {
|
|
||||||
String errorMsg = getMsg("sop.error.indirect_data_type.illegal_use_of_env_designator");
|
|
||||||
throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// File Descriptor
|
|
||||||
if (trimmed.startsWith(PRFX_FD)) {
|
|
||||||
File fdFile = fileDescriptorFromString(trimmed);
|
|
||||||
try {
|
|
||||||
FileOutputStream fout = new FileOutputStream(fdFile);
|
|
||||||
return fout;
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
String errorMsg = getMsg("sop.error.indirect_data_type.file_descriptor_not_found", fdFile.getAbsolutePath());
|
|
||||||
throw new IOException(errorMsg, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
File file = new File(trimmed);
|
|
||||||
if (file.exists()) {
|
|
||||||
String errorMsg = getMsg("sop.error.indirect_data_type.output_file_already_exists", file.getAbsolutePath());
|
|
||||||
throw new SOPGPException.OutputExists(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.createNewFile()) {
|
|
||||||
String errorMsg = getMsg("sop.error.indirect_data_type.output_file_cannot_be_created", file.getAbsolutePath());
|
|
||||||
throw new IOException(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new FileOutputStream(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
public File fileDescriptorFromString(String fdString) {
|
|
||||||
File fdDir = new File("/dev/fd/");
|
|
||||||
if (!fdDir.exists()) {
|
|
||||||
String errorMsg = getMsg("sop.error.indirect_data_type.designator_fd_not_supported");
|
|
||||||
throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg);
|
|
||||||
}
|
|
||||||
String fdNumber = fdString.substring(PRFX_FD.length());
|
|
||||||
if (!PATTERN_FD.matcher(fdNumber).matches()) {
|
|
||||||
throw new IllegalArgumentException("File descriptor must be a positive number.");
|
|
||||||
}
|
|
||||||
File descriptor = new File(fdDir, fdNumber);
|
|
||||||
return descriptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String stringFromInputStream(InputStream inputStream) throws IOException {
|
|
||||||
try {
|
|
||||||
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
|
|
||||||
byte[] buf = new byte[4096]; int read;
|
|
||||||
while ((read = inputStream.read(buf)) != -1) {
|
|
||||||
byteOut.write(buf, 0, read);
|
|
||||||
}
|
|
||||||
// TODO: For decrypt operations we MUST accept non-UTF8 passwords
|
|
||||||
return UTF8Util.decodeUTF8(byteOut.toByteArray());
|
|
||||||
} finally {
|
|
||||||
inputStream.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date parseNotAfter(String notAfter) {
|
|
||||||
if (notAfter.equals("now")) {
|
|
||||||
return new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notAfter.equals("-")) {
|
|
||||||
return END_OF_TIME;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return UTCUtil.parseUTCDate(notAfter);
|
|
||||||
} catch (ParseException e) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.malformed_not_after");
|
|
||||||
throw new IllegalArgumentException(errorMsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date parseNotBefore(String notBefore) {
|
|
||||||
if (notBefore.equals("now")) {
|
|
||||||
return new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notBefore.equals("-")) {
|
|
||||||
return BEGINNING_OF_TIME;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return UTCUtil.parseUTCDate(notBefore);
|
|
||||||
} catch (ParseException e) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.malformed_not_before");
|
|
||||||
throw new IllegalArgumentException(errorMsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
|
||||||
|
|
||||||
import picocli.CommandLine;
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.cli.picocli.SopCLI;
|
|
||||||
import sop.enums.ArmorLabel;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.Armor;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@CommandLine.Command(name = "armor",
|
|
||||||
resourceBundle = "msg_armor",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public class ArmorCmd extends AbstractSopCmd {
|
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--label"},
|
|
||||||
paramLabel = "{auto|sig|key|cert|message}")
|
|
||||||
ArmorLabel label;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Armor armor = throwIfUnsupportedSubcommand(
|
|
||||||
SopCLI.getSop().armor(),
|
|
||||||
"armor");
|
|
||||||
|
|
||||||
if (label != null) {
|
|
||||||
try {
|
|
||||||
armor.label(label);
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--label");
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Ready ready = armor.data(System.in);
|
|
||||||
ready.writeTo(System.out);
|
|
||||||
} catch (SOPGPException.BadData badData) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data");
|
|
||||||
throw new SOPGPException.BadData(errorMsg, badData);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
|
||||||
|
|
||||||
import picocli.CommandLine;
|
|
||||||
import sop.cli.picocli.SopCLI;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.ChangeKeyPassword;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@CommandLine.Command(name = "change-key-password",
|
|
||||||
resourceBundle = "msg_change-key-password",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public class ChangeKeyPasswordCmd extends AbstractSopCmd {
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--no-armor",
|
|
||||||
negatable = true)
|
|
||||||
boolean armor = true;
|
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--old-key-password"},
|
|
||||||
paramLabel = "PASSWORD")
|
|
||||||
List<String> oldKeyPasswords = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--new-key-password"}, arity = "0..1",
|
|
||||||
paramLabel = "PASSWORD")
|
|
||||||
String newKeyPassword = null;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
ChangeKeyPassword changeKeyPassword = throwIfUnsupportedSubcommand(
|
|
||||||
SopCLI.getSop().changeKeyPassword(), "change-key-password");
|
|
||||||
|
|
||||||
if (!armor) {
|
|
||||||
changeKeyPassword.noArmor();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String oldKeyPassword : oldKeyPasswords) {
|
|
||||||
changeKeyPassword.oldKeyPassphrase(oldKeyPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newKeyPassword != null) {
|
|
||||||
changeKeyPassword.newKeyPassphrase(newKeyPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
changeKeyPassword.keys(System.in).writeTo(System.out);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
// 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.SopCLI;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.Dearmor;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@CommandLine.Command(name = "dearmor",
|
|
||||||
resourceBundle = "msg_dearmor",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public class DearmorCmd extends AbstractSopCmd {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Dearmor dearmor = throwIfUnsupportedSubcommand(
|
|
||||||
SopCLI.getSop().dearmor(), "dearmor");
|
|
||||||
|
|
||||||
try {
|
|
||||||
dearmor.data(System.in)
|
|
||||||
.writeTo(System.out);
|
|
||||||
} catch (SOPGPException.BadData e) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data");
|
|
||||||
throw new SOPGPException.BadData(errorMsg, e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
String msg = e.getMessage();
|
|
||||||
if (msg == null) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data");
|
|
||||||
if (msg.equals("invalid armor") ||
|
|
||||||
msg.equals("invalid armor header") ||
|
|
||||||
msg.equals("inconsistent line endings in headers") ||
|
|
||||||
msg.startsWith("unable to decode base64 data")) {
|
|
||||||
throw new SOPGPException.BadData(errorMsg, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,255 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
|
||||||
|
|
||||||
import picocli.CommandLine;
|
|
||||||
import sop.DecryptionResult;
|
|
||||||
import sop.ReadyWithResult;
|
|
||||||
import sop.SessionKey;
|
|
||||||
import sop.Verification;
|
|
||||||
import sop.cli.picocli.SopCLI;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.Decrypt;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@CommandLine.Command(name = "decrypt",
|
|
||||||
resourceBundle = "msg_decrypt",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public class DecryptCmd extends AbstractSopCmd {
|
|
||||||
|
|
||||||
private static final String OPT_SESSION_KEY_OUT = "--session-key-out";
|
|
||||||
private static final String OPT_WITH_SESSION_KEY = "--with-session-key";
|
|
||||||
private static final String OPT_WITH_PASSWORD = "--with-password";
|
|
||||||
private static final String OPT_WITH_KEY_PASSWORD = "--with-key-password";
|
|
||||||
private static final String OPT_VERIFICATIONS_OUT = "--verifications-out"; // see SOP-05
|
|
||||||
private static final String OPT_VERIFY_WITH = "--verify-with";
|
|
||||||
private static final String OPT_NOT_BEFORE = "--verify-not-before";
|
|
||||||
private static final String OPT_NOT_AFTER = "--verify-not-after";
|
|
||||||
|
|
||||||
|
|
||||||
@CommandLine.Option(
|
|
||||||
names = {OPT_SESSION_KEY_OUT},
|
|
||||||
paramLabel = "SESSIONKEY")
|
|
||||||
String sessionKeyOut;
|
|
||||||
|
|
||||||
@CommandLine.Option(
|
|
||||||
names = {OPT_WITH_SESSION_KEY},
|
|
||||||
paramLabel = "SESSIONKEY")
|
|
||||||
List<String> withSessionKey = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(
|
|
||||||
names = {OPT_WITH_PASSWORD},
|
|
||||||
paramLabel = "PASSWORD")
|
|
||||||
List<String> withPassword = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = {OPT_VERIFICATIONS_OUT, "--verify-out"}, // TODO: Remove --verify-out in 06
|
|
||||||
paramLabel = "VERIFICATIONS")
|
|
||||||
String verifyOut;
|
|
||||||
|
|
||||||
@CommandLine.Option(names = {OPT_VERIFY_WITH},
|
|
||||||
paramLabel = "CERT")
|
|
||||||
List<String> certs = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = {OPT_NOT_BEFORE},
|
|
||||||
paramLabel = "DATE")
|
|
||||||
String notBefore = "-";
|
|
||||||
|
|
||||||
@CommandLine.Option(names = {OPT_NOT_AFTER},
|
|
||||||
paramLabel = "DATE")
|
|
||||||
String notAfter = "now";
|
|
||||||
|
|
||||||
@CommandLine.Parameters(index = "0..*",
|
|
||||||
paramLabel = "KEY")
|
|
||||||
List<String> keys = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = {OPT_WITH_KEY_PASSWORD},
|
|
||||||
paramLabel = "PASSWORD")
|
|
||||||
List<String> withKeyPassword = new ArrayList<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Decrypt decrypt = throwIfUnsupportedSubcommand(
|
|
||||||
SopCLI.getSop().decrypt(), "decrypt");
|
|
||||||
|
|
||||||
throwIfOutputExists(verifyOut);
|
|
||||||
throwIfOutputExists(sessionKeyOut);
|
|
||||||
|
|
||||||
setNotAfter(notAfter, decrypt);
|
|
||||||
setNotBefore(notBefore, decrypt);
|
|
||||||
setWithPasswords(withPassword, decrypt);
|
|
||||||
setWithSessionKeys(withSessionKey, decrypt);
|
|
||||||
setWithKeyPassword(withKeyPassword, decrypt);
|
|
||||||
setVerifyWith(certs, decrypt);
|
|
||||||
setDecryptWith(keys, decrypt);
|
|
||||||
|
|
||||||
if (verifyOut != null && certs.isEmpty()) {
|
|
||||||
String errorMsg = getMsg("sop.error.usage.option_requires_other_option", OPT_VERIFICATIONS_OUT, OPT_VERIFY_WITH);
|
|
||||||
throw new SOPGPException.IncompleteVerification(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
ReadyWithResult<DecryptionResult> ready = decrypt.ciphertext(System.in);
|
|
||||||
DecryptionResult result = ready.writeTo(System.out);
|
|
||||||
writeSessionKeyOut(result);
|
|
||||||
writeVerifyOut(result);
|
|
||||||
|
|
||||||
} catch (SOPGPException.BadData badData) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.stdin_not_a_message");
|
|
||||||
throw new SOPGPException.BadData(errorMsg, badData);
|
|
||||||
} catch (SOPGPException.CannotDecrypt e) {
|
|
||||||
String errorMsg = getMsg("sop.error.runtime.cannot_decrypt_message");
|
|
||||||
throw new SOPGPException.CannotDecrypt(errorMsg, e);
|
|
||||||
} catch (IOException ioException) {
|
|
||||||
throw new RuntimeException(ioException);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeVerifyOut(DecryptionResult result) throws IOException {
|
|
||||||
if (verifyOut != null) {
|
|
||||||
if (result.getVerifications().isEmpty()) {
|
|
||||||
String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found");
|
|
||||||
throw new SOPGPException.NoSignature(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
try (OutputStream fileOut = getOutput(verifyOut)) {
|
|
||||||
PrintWriter writer = new PrintWriter(fileOut);
|
|
||||||
for (Verification verification : result.getVerifications()) {
|
|
||||||
// CHECKSTYLE:OFF
|
|
||||||
writer.println(verification.toString());
|
|
||||||
// CHECKSTYLE:ON
|
|
||||||
}
|
|
||||||
writer.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeSessionKeyOut(DecryptionResult result) throws IOException {
|
|
||||||
if (sessionKeyOut == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try (OutputStream outputStream = getOutput(sessionKeyOut)) {
|
|
||||||
if (!result.getSessionKey().isPresent()) {
|
|
||||||
String errorMsg = getMsg("sop.error.runtime.no_session_key_extracted");
|
|
||||||
throw new SOPGPException.UnsupportedOption(String.format(errorMsg, OPT_SESSION_KEY_OUT));
|
|
||||||
}
|
|
||||||
SessionKey sessionKey = result.getSessionKey().get();
|
|
||||||
PrintWriter writer = new PrintWriter(outputStream);
|
|
||||||
// CHECKSTYLE:OFF
|
|
||||||
writer.println(sessionKey.toString());
|
|
||||||
// CHECKSTYLE:ON
|
|
||||||
writer.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setDecryptWith(List<String> keys, Decrypt decrypt) {
|
|
||||||
for (String key : keys) {
|
|
||||||
try (InputStream keyIn = getInput(key)) {
|
|
||||||
decrypt.withKey(keyIn);
|
|
||||||
} catch (SOPGPException.KeyIsProtected keyIsProtected) {
|
|
||||||
String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", key);
|
|
||||||
throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected);
|
|
||||||
} catch (SOPGPException.BadData badData) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.not_a_private_key", key);
|
|
||||||
throw new SOPGPException.BadData(errorMsg, badData);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setVerifyWith(List<String> certs, Decrypt decrypt) {
|
|
||||||
for (String cert : certs) {
|
|
||||||
try (InputStream certIn = getInput(cert)) {
|
|
||||||
decrypt.verifyWithCert(certIn);
|
|
||||||
} catch (SOPGPException.BadData badData) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.not_a_certificate", cert);
|
|
||||||
throw new SOPGPException.BadData(errorMsg, badData);
|
|
||||||
} catch (IOException ioException) {
|
|
||||||
throw new RuntimeException(ioException);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setWithSessionKeys(List<String> withSessionKey, Decrypt decrypt) {
|
|
||||||
for (String sessionKeyFile : withSessionKey) {
|
|
||||||
String sessionKeyString;
|
|
||||||
try {
|
|
||||||
sessionKeyString = stringFromInputStream(getInput(sessionKeyFile));
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
SessionKey sessionKey;
|
|
||||||
try {
|
|
||||||
sessionKey = SessionKey.fromString(sessionKeyString);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.malformed_session_key");
|
|
||||||
throw new IllegalArgumentException(errorMsg, e);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
decrypt.withSessionKey(sessionKey);
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_SESSION_KEY);
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setWithPasswords(List<String> withPassword, Decrypt decrypt) {
|
|
||||||
for (String passwordFile : withPassword) {
|
|
||||||
try {
|
|
||||||
String password = stringFromInputStream(getInput(passwordFile));
|
|
||||||
decrypt.withPassword(password);
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_PASSWORD);
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setWithKeyPassword(List<String> withKeyPassword, Decrypt decrypt) {
|
|
||||||
for (String passwordFile : withKeyPassword) {
|
|
||||||
try {
|
|
||||||
String password = stringFromInputStream(getInput(passwordFile));
|
|
||||||
decrypt.withKeyPassword(password);
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_KEY_PASSWORD);
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setNotAfter(String notAfter, Decrypt decrypt) {
|
|
||||||
Date notAfterDate = parseNotAfter(notAfter);
|
|
||||||
try {
|
|
||||||
decrypt.verifyNotAfter(notAfterDate);
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_AFTER);
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setNotBefore(String notBefore, Decrypt decrypt) {
|
|
||||||
Date notBeforeDate = parseNotBefore(notBefore);
|
|
||||||
try {
|
|
||||||
decrypt.verifyNotBefore(notBeforeDate);
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_BEFORE);
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,154 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
|
||||||
|
|
||||||
import picocli.CommandLine;
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.cli.picocli.SopCLI;
|
|
||||||
import sop.enums.EncryptAs;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.Encrypt;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@CommandLine.Command(name = "encrypt",
|
|
||||||
resourceBundle = "msg_encrypt",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public class EncryptCmd extends AbstractSopCmd {
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--no-armor",
|
|
||||||
negatable = true)
|
|
||||||
boolean armor = true;
|
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--as"},
|
|
||||||
paramLabel = "{binary|text}")
|
|
||||||
EncryptAs type;
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--with-password",
|
|
||||||
paramLabel = "PASSWORD")
|
|
||||||
List<String> withPassword = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--sign-with",
|
|
||||||
paramLabel = "KEY")
|
|
||||||
List<String> signWith = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--with-key-password",
|
|
||||||
paramLabel = "PASSWORD")
|
|
||||||
List<String> withKeyPassword = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--profile",
|
|
||||||
paramLabel = "PROFILE")
|
|
||||||
String profile;
|
|
||||||
|
|
||||||
@CommandLine.Parameters(index = "0..*",
|
|
||||||
paramLabel = "CERTS")
|
|
||||||
List<String> certs = new ArrayList<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Encrypt encrypt = throwIfUnsupportedSubcommand(
|
|
||||||
SopCLI.getSop().encrypt(), "encrypt");
|
|
||||||
|
|
||||||
if (profile != null) {
|
|
||||||
try {
|
|
||||||
encrypt.profile(profile);
|
|
||||||
} catch (SOPGPException.UnsupportedProfile e) {
|
|
||||||
String errorMsg = getMsg("sop.error.usage.profile_not_supported", "encrypt", profile);
|
|
||||||
throw new SOPGPException.UnsupportedProfile(errorMsg, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type != null) {
|
|
||||||
try {
|
|
||||||
encrypt.mode(type);
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as");
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (withPassword.isEmpty() && certs.isEmpty()) {
|
|
||||||
String errorMsg = getMsg("sop.error.usage.password_or_cert_required");
|
|
||||||
throw new SOPGPException.MissingArg(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String passwordFileName : withPassword) {
|
|
||||||
try {
|
|
||||||
String password = stringFromInputStream(getInput(passwordFileName));
|
|
||||||
encrypt.withPassword(password);
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-password");
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String passwordFileName : withKeyPassword) {
|
|
||||||
try {
|
|
||||||
String password = stringFromInputStream(getInput(passwordFileName));
|
|
||||||
encrypt.withKeyPassword(password);
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password");
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String keyInput : signWith) {
|
|
||||||
try (InputStream keyIn = getInput(keyInput)) {
|
|
||||||
encrypt.signWith(keyIn);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (SOPGPException.KeyIsProtected keyIsProtected) {
|
|
||||||
String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput);
|
|
||||||
throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected);
|
|
||||||
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
|
|
||||||
String errorMsg = getMsg("sop.error.runtime.key_uses_unsupported_asymmetric_algorithm", keyInput);
|
|
||||||
throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo);
|
|
||||||
} catch (SOPGPException.KeyCannotSign keyCannotSign) {
|
|
||||||
String errorMsg = getMsg("sop.error.runtime.key_cannot_sign", keyInput);
|
|
||||||
throw new SOPGPException.KeyCannotSign(errorMsg, keyCannotSign);
|
|
||||||
} catch (SOPGPException.BadData badData) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput);
|
|
||||||
throw new SOPGPException.BadData(errorMsg, badData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String certInput : certs) {
|
|
||||||
try (InputStream certIn = getInput(certInput)) {
|
|
||||||
encrypt.withCert(certIn);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
|
|
||||||
String errorMsg = getMsg("sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput);
|
|
||||||
throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo);
|
|
||||||
} catch (SOPGPException.CertCannotEncrypt certCannotEncrypt) {
|
|
||||||
String errorMsg = getMsg("sop.error.runtime.cert_cannot_encrypt", certInput);
|
|
||||||
throw new SOPGPException.CertCannotEncrypt(errorMsg, certCannotEncrypt);
|
|
||||||
} catch (SOPGPException.BadData badData) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput);
|
|
||||||
throw new SOPGPException.BadData(errorMsg, badData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!armor) {
|
|
||||||
encrypt.noArmor();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Ready ready = encrypt.plaintext(System.in);
|
|
||||||
ready.writeTo(System.out);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
// 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",
|
|
||||||
resourceBundle = "msg_extract-cert",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public class ExtractCertCmd extends AbstractSopCmd {
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--no-armor",
|
|
||||||
negatable = true)
|
|
||||||
boolean armor = true;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
ExtractCert extractCert = throwIfUnsupportedSubcommand(
|
|
||||||
SopCLI.getSop().extractCert(), "extract-cert");
|
|
||||||
|
|
||||||
if (!armor) {
|
|
||||||
extractCert.noArmor();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Ready ready = extractCert.key(System.in);
|
|
||||||
ready.writeTo(System.out);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (SOPGPException.BadData badData) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.stdin_not_a_private_key");
|
|
||||||
throw new SOPGPException.BadData(errorMsg, badData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
|
||||||
|
|
||||||
import picocli.CommandLine;
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.cli.picocli.SopCLI;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.GenerateKey;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@CommandLine.Command(name = "generate-key",
|
|
||||||
resourceBundle = "msg_generate-key",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public class GenerateKeyCmd extends AbstractSopCmd {
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--no-armor",
|
|
||||||
negatable = true)
|
|
||||||
boolean armor = true;
|
|
||||||
|
|
||||||
@CommandLine.Parameters(paramLabel = "USERID")
|
|
||||||
List<String> userId = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--with-key-password",
|
|
||||||
paramLabel = "PASSWORD")
|
|
||||||
String withKeyPassword;
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--profile",
|
|
||||||
paramLabel = "PROFILE")
|
|
||||||
String profile;
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--signing-only")
|
|
||||||
boolean signingOnly = false;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
GenerateKey generateKey = throwIfUnsupportedSubcommand(
|
|
||||||
SopCLI.getSop().generateKey(), "generate-key");
|
|
||||||
|
|
||||||
if (profile != null) {
|
|
||||||
try {
|
|
||||||
generateKey.profile(profile);
|
|
||||||
} catch (SOPGPException.UnsupportedProfile e) {
|
|
||||||
String errorMsg = getMsg("sop.error.usage.profile_not_supported", "generate-key", profile);
|
|
||||||
throw new SOPGPException.UnsupportedProfile(errorMsg, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signingOnly) {
|
|
||||||
generateKey.signingOnly();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String userId : userId) {
|
|
||||||
generateKey.userId(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!armor) {
|
|
||||||
generateKey.noArmor();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (withKeyPassword != null) {
|
|
||||||
try {
|
|
||||||
String password = stringFromInputStream(getInput(withKeyPassword));
|
|
||||||
generateKey.withKeyPassword(password);
|
|
||||||
} catch (SOPGPException.UnsupportedOption e) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password");
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Ready ready = generateKey.generate();
|
|
||||||
ready.writeTo(System.out);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
|
||||||
|
|
||||||
import picocli.CommandLine;
|
|
||||||
import sop.Signatures;
|
|
||||||
import sop.cli.picocli.SopCLI;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.InlineDetach;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
@CommandLine.Command(name = "inline-detach",
|
|
||||||
resourceBundle = "msg_inline-detach",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public class InlineDetachCmd extends AbstractSopCmd {
|
|
||||||
|
|
||||||
@CommandLine.Option(
|
|
||||||
names = {"--signatures-out"},
|
|
||||||
paramLabel = "SIGNATURES")
|
|
||||||
String signaturesOut;
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--no-armor",
|
|
||||||
negatable = true)
|
|
||||||
boolean armor = true;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
InlineDetach inlineDetach = throwIfUnsupportedSubcommand(
|
|
||||||
SopCLI.getSop().inlineDetach(), "inline-detach");
|
|
||||||
|
|
||||||
throwIfOutputExists(signaturesOut);
|
|
||||||
throwIfMissingArg(signaturesOut, "--signatures-out");
|
|
||||||
|
|
||||||
if (!armor) {
|
|
||||||
inlineDetach.noArmor();
|
|
||||||
}
|
|
||||||
|
|
||||||
try (OutputStream outputStream = getOutput(signaturesOut)) {
|
|
||||||
Signatures signatures = inlineDetach
|
|
||||||
.message(System.in).writeTo(System.out);
|
|
||||||
signatures.writeTo(outputStream);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
|
||||||
|
|
||||||
import picocli.CommandLine;
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.cli.picocli.SopCLI;
|
|
||||||
import sop.enums.InlineSignAs;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.InlineSign;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@CommandLine.Command(name = "inline-sign",
|
|
||||||
resourceBundle = "msg_inline-sign",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public class InlineSignCmd extends AbstractSopCmd {
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--no-armor",
|
|
||||||
negatable = true)
|
|
||||||
boolean armor = true;
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--as",
|
|
||||||
paramLabel = "{binary|text|clearsigned}")
|
|
||||||
InlineSignAs type;
|
|
||||||
|
|
||||||
@CommandLine.Parameters(paramLabel = "KEYS")
|
|
||||||
List<String> secretKeyFile = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--with-key-password",
|
|
||||||
paramLabel = "PASSWORD")
|
|
||||||
List<String> withKeyPassword = new ArrayList<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
InlineSign inlineSign = throwIfUnsupportedSubcommand(
|
|
||||||
SopCLI.getSop().inlineSign(), "inline-sign");
|
|
||||||
|
|
||||||
// Clearsigned messages are inherently armored, so --no-armor makes no sense.
|
|
||||||
if (!armor && type == InlineSignAs.clearsigned) {
|
|
||||||
String errorMsg = getMsg("sop.error.usage.incompatible_options.clearsigned_no_armor");
|
|
||||||
throw new SOPGPException.IncompatibleOptions(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type != null) {
|
|
||||||
try {
|
|
||||||
inlineSign.mode(type);
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as");
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (secretKeyFile.isEmpty()) {
|
|
||||||
String errorMsg = getMsg("sop.error.usage.parameter_required", "KEYS");
|
|
||||||
throw new SOPGPException.MissingArg(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String passwordFile : withKeyPassword) {
|
|
||||||
try {
|
|
||||||
String password = stringFromInputStream(getInput(passwordFile));
|
|
||||||
inlineSign.withKeyPassword(password);
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password");
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String keyInput : secretKeyFile) {
|
|
||||||
try (InputStream keyIn = getInput(keyInput)) {
|
|
||||||
inlineSign.key(keyIn);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (SOPGPException.KeyIsProtected e) {
|
|
||||||
String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput);
|
|
||||||
throw new SOPGPException.KeyIsProtected(errorMsg, e);
|
|
||||||
} catch (SOPGPException.BadData badData) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput);
|
|
||||||
throw new SOPGPException.BadData(errorMsg, badData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!armor) {
|
|
||||||
inlineSign.noArmor();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Ready ready = inlineSign.data(System.in);
|
|
||||||
ready.writeTo(System.out);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
|
||||||
|
|
||||||
import picocli.CommandLine;
|
|
||||||
import sop.ReadyWithResult;
|
|
||||||
import sop.Verification;
|
|
||||||
import sop.cli.picocli.SopCLI;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.InlineVerify;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@CommandLine.Command(name = "inline-verify",
|
|
||||||
resourceBundle = "msg_inline-verify",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public class InlineVerifyCmd extends AbstractSopCmd {
|
|
||||||
|
|
||||||
@CommandLine.Parameters(arity = "0..*",
|
|
||||||
paramLabel = "CERT")
|
|
||||||
List<String> certificates = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--not-before"},
|
|
||||||
paramLabel = "DATE")
|
|
||||||
String notBefore = "-";
|
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--not-after"},
|
|
||||||
paramLabel = "DATE")
|
|
||||||
String notAfter = "now";
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--verifications-out", paramLabel = "VERIFICATIONS")
|
|
||||||
String verificationsOut;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
InlineVerify inlineVerify = throwIfUnsupportedSubcommand(
|
|
||||||
SopCLI.getSop().inlineVerify(), "inline-verify");
|
|
||||||
|
|
||||||
throwIfOutputExists(verificationsOut);
|
|
||||||
|
|
||||||
if (notAfter != null) {
|
|
||||||
try {
|
|
||||||
inlineVerify.notAfter(parseNotAfter(notAfter));
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after");
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (notBefore != null) {
|
|
||||||
try {
|
|
||||||
inlineVerify.notBefore(parseNotBefore(notBefore));
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before");
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String certInput : certificates) {
|
|
||||||
try (InputStream certIn = getInput(certInput)) {
|
|
||||||
inlineVerify.cert(certIn);
|
|
||||||
} catch (IOException ioException) {
|
|
||||||
throw new RuntimeException(ioException);
|
|
||||||
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
|
|
||||||
String errorMsg = getMsg("sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput);
|
|
||||||
throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo);
|
|
||||||
} catch (SOPGPException.BadData badData) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput);
|
|
||||||
throw new SOPGPException.BadData(errorMsg, badData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Verification> verifications = null;
|
|
||||||
try {
|
|
||||||
ReadyWithResult<List<Verification>> ready = inlineVerify.data(System.in);
|
|
||||||
verifications = ready.writeTo(System.out);
|
|
||||||
} catch (SOPGPException.NoSignature e) {
|
|
||||||
String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found");
|
|
||||||
throw new SOPGPException.NoSignature(errorMsg, e);
|
|
||||||
} catch (IOException ioException) {
|
|
||||||
throw new RuntimeException(ioException);
|
|
||||||
} catch (SOPGPException.BadData badData) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.stdin_not_a_message");
|
|
||||||
throw new SOPGPException.BadData(errorMsg, badData);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (verificationsOut != null) {
|
|
||||||
try (OutputStream outputStream = getOutput(verificationsOut)) {
|
|
||||||
PrintWriter pw = new PrintWriter(outputStream);
|
|
||||||
for (Verification verification : verifications) {
|
|
||||||
// CHECKSTYLE:OFF
|
|
||||||
pw.println(verification);
|
|
||||||
// CHECKSTYLE:ON
|
|
||||||
}
|
|
||||||
pw.flush();
|
|
||||||
pw.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
|
||||||
|
|
||||||
import picocli.CommandLine;
|
|
||||||
import sop.Profile;
|
|
||||||
import sop.cli.picocli.Print;
|
|
||||||
import sop.cli.picocli.SopCLI;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.ListProfiles;
|
|
||||||
|
|
||||||
@CommandLine.Command(name = "list-profiles",
|
|
||||||
resourceBundle = "msg_list-profiles",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public class ListProfilesCmd extends AbstractSopCmd {
|
|
||||||
|
|
||||||
@CommandLine.Parameters(paramLabel = "COMMAND", arity = "1", descriptionKey = "subcommand")
|
|
||||||
String subcommand;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
ListProfiles listProfiles = throwIfUnsupportedSubcommand(
|
|
||||||
SopCLI.getSop().listProfiles(), "list-profiles");
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (Profile profile : listProfiles.subcommand(subcommand)) {
|
|
||||||
Print.outln(profile.toString());
|
|
||||||
}
|
|
||||||
} catch (SOPGPException.UnsupportedProfile e) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.subcommand_does_not_support_profiles", subcommand);
|
|
||||||
throw new SOPGPException.UnsupportedProfile(errorMsg, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
|
||||||
|
|
||||||
import picocli.CommandLine;
|
|
||||||
import sop.Ready;
|
|
||||||
import sop.cli.picocli.SopCLI;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.RevokeKey;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@CommandLine.Command(name = "revoke-key",
|
|
||||||
resourceBundle = "msg_revoke-key",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public class RevokeKeyCmd extends AbstractSopCmd {
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--no-armor",
|
|
||||||
negatable = true)
|
|
||||||
boolean armor = true;
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--with-key-password",
|
|
||||||
paramLabel = "PASSWORD")
|
|
||||||
String withKeyPassword;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
RevokeKey revokeKey = throwIfUnsupportedSubcommand(
|
|
||||||
SopCLI.getSop().revokeKey(), "revoke-key");
|
|
||||||
|
|
||||||
if (!armor) {
|
|
||||||
revokeKey.noArmor();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (withKeyPassword != null) {
|
|
||||||
try {
|
|
||||||
String password = stringFromInputStream(getInput(withKeyPassword));
|
|
||||||
revokeKey.withKeyPassword(password);
|
|
||||||
} catch (SOPGPException.UnsupportedOption e) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password");
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ready ready;
|
|
||||||
try {
|
|
||||||
ready = revokeKey.keys(System.in);
|
|
||||||
} catch (SOPGPException.KeyIsProtected e) {
|
|
||||||
String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", "STANDARD_IN");
|
|
||||||
throw new SOPGPException.KeyIsProtected(errorMsg, e);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
ready.writeTo(System.out);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
|
||||||
|
|
||||||
import picocli.CommandLine;
|
|
||||||
import sop.MicAlg;
|
|
||||||
import sop.ReadyWithResult;
|
|
||||||
import sop.SigningResult;
|
|
||||||
import sop.cli.picocli.SopCLI;
|
|
||||||
import sop.enums.SignAs;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.DetachedSign;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@CommandLine.Command(name = "sign",
|
|
||||||
resourceBundle = "msg_detached-sign",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public class SignCmd extends AbstractSopCmd {
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--no-armor",
|
|
||||||
negatable = true)
|
|
||||||
boolean armor = true;
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--as",
|
|
||||||
paramLabel = "{binary|text}")
|
|
||||||
SignAs type;
|
|
||||||
|
|
||||||
@CommandLine.Parameters(paramLabel = "KEYS")
|
|
||||||
List<String> secretKeyFile = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--with-key-password",
|
|
||||||
paramLabel = "PASSWORD")
|
|
||||||
List<String> withKeyPassword = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--micalg-out",
|
|
||||||
paramLabel = "MICALG")
|
|
||||||
String micAlgOut;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
DetachedSign detachedSign = throwIfUnsupportedSubcommand(
|
|
||||||
SopCLI.getSop().detachedSign(), "sign");
|
|
||||||
|
|
||||||
throwIfOutputExists(micAlgOut);
|
|
||||||
throwIfEmptyParameters(secretKeyFile, "KEYS");
|
|
||||||
|
|
||||||
if (type != null) {
|
|
||||||
try {
|
|
||||||
detachedSign.mode(type);
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as");
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String passwordFile : withKeyPassword) {
|
|
||||||
try {
|
|
||||||
String password = stringFromInputStream(getInput(passwordFile));
|
|
||||||
detachedSign.withKeyPassword(password);
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password");
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String keyInput : secretKeyFile) {
|
|
||||||
try (InputStream keyIn = getInput(keyInput)) {
|
|
||||||
detachedSign.key(keyIn);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (SOPGPException.KeyIsProtected keyIsProtected) {
|
|
||||||
String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput);
|
|
||||||
throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected);
|
|
||||||
} catch (SOPGPException.BadData badData) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput);
|
|
||||||
throw new SOPGPException.BadData(errorMsg, badData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!armor) {
|
|
||||||
detachedSign.noArmor();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
ReadyWithResult<SigningResult> ready = detachedSign.data(System.in);
|
|
||||||
SigningResult result = ready.writeTo(System.out);
|
|
||||||
|
|
||||||
MicAlg micAlg = result.getMicAlg();
|
|
||||||
if (micAlgOut != null) {
|
|
||||||
// Write micalg out
|
|
||||||
OutputStream outputStream = getOutput(micAlgOut);
|
|
||||||
micAlg.writeTo(outputStream);
|
|
||||||
outputStream.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
|
||||||
|
|
||||||
import picocli.CommandLine;
|
|
||||||
import sop.Verification;
|
|
||||||
import sop.cli.picocli.Print;
|
|
||||||
import sop.cli.picocli.SopCLI;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.DetachedVerify;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@CommandLine.Command(name = "verify",
|
|
||||||
resourceBundle = "msg_detached-verify",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public class VerifyCmd extends AbstractSopCmd {
|
|
||||||
|
|
||||||
@CommandLine.Parameters(index = "0",
|
|
||||||
paramLabel = "SIGNATURE")
|
|
||||||
String signature;
|
|
||||||
|
|
||||||
@CommandLine.Parameters(index = "1..*",
|
|
||||||
arity = "1..*",
|
|
||||||
paramLabel = "CERT")
|
|
||||||
List<String> certificates = new ArrayList<>();
|
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--not-before"},
|
|
||||||
paramLabel = "DATE")
|
|
||||||
String notBefore = "-";
|
|
||||||
|
|
||||||
@CommandLine.Option(names = {"--not-after"},
|
|
||||||
paramLabel = "DATE")
|
|
||||||
String notAfter = "now";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
DetachedVerify detachedVerify = throwIfUnsupportedSubcommand(
|
|
||||||
SopCLI.getSop().detachedVerify(), "verify");
|
|
||||||
|
|
||||||
if (notAfter != null) {
|
|
||||||
try {
|
|
||||||
detachedVerify.notAfter(parseNotAfter(notAfter));
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after");
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (notBefore != null) {
|
|
||||||
try {
|
|
||||||
detachedVerify.notBefore(parseNotBefore(notBefore));
|
|
||||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
|
||||||
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before");
|
|
||||||
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String certInput : certificates) {
|
|
||||||
try (InputStream certIn = getInput(certInput)) {
|
|
||||||
detachedVerify.cert(certIn);
|
|
||||||
} catch (IOException ioException) {
|
|
||||||
throw new RuntimeException(ioException);
|
|
||||||
} catch (SOPGPException.BadData badData) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput);
|
|
||||||
throw new SOPGPException.BadData(errorMsg, badData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signature != null) {
|
|
||||||
try (InputStream sigIn = getInput(signature)) {
|
|
||||||
detachedVerify.signatures(sigIn);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (SOPGPException.BadData badData) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.not_a_signature", signature);
|
|
||||||
throw new SOPGPException.BadData(errorMsg, badData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Verification> verifications;
|
|
||||||
try {
|
|
||||||
verifications = detachedVerify.data(System.in);
|
|
||||||
} catch (SOPGPException.NoSignature e) {
|
|
||||||
String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found");
|
|
||||||
throw new SOPGPException.NoSignature(errorMsg, e);
|
|
||||||
} catch (IOException ioException) {
|
|
||||||
throw new RuntimeException(ioException);
|
|
||||||
} catch (SOPGPException.BadData badData) {
|
|
||||||
String errorMsg = getMsg("sop.error.input.stdin_not_a_message");
|
|
||||||
throw new SOPGPException.BadData(errorMsg, badData);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Verification verification : verifications) {
|
|
||||||
Print.outln(verification.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
// 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", resourceBundle = "msg_version",
|
|
||||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public class VersionCmd extends AbstractSopCmd {
|
|
||||||
|
|
||||||
@CommandLine.ArgGroup()
|
|
||||||
Exclusive exclusive;
|
|
||||||
|
|
||||||
static class Exclusive {
|
|
||||||
@CommandLine.Option(names = "--extended")
|
|
||||||
boolean extended;
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--backend")
|
|
||||||
boolean backend;
|
|
||||||
|
|
||||||
@CommandLine.Option(names = "--sop-spec")
|
|
||||||
boolean sopSpec;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Version version = throwIfUnsupportedSubcommand(
|
|
||||||
SopCLI.getSop().version(), "version");
|
|
||||||
|
|
||||||
if (exclusive == null) {
|
|
||||||
Print.outln(version.getName() + " " + version.getVersion());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exclusive.extended) {
|
|
||||||
Print.outln(version.getExtendedVersion());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exclusive.backend) {
|
|
||||||
Print.outln(version.getBackendVersion());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exclusive.sopSpec) {
|
|
||||||
Print.outln(version.getSopSpecVersion());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subcommands of the PGPainless SOP.
|
|
||||||
*/
|
|
||||||
package sop.cli.picocli.commands;
|
|
|
@ -1,8 +0,0 @@
|
||||||
// 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;
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
// 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 MissingParameterException) {
|
||||||
|
SOPGPException.MissingArg.EXIT_CODE
|
||||||
|
} else if (exception is ParameterException) {
|
||||||
|
// Invalid option (e.g. `--as invalid`)
|
||||||
|
SOPGPException.UnsupportedOption.EXIT_CODE
|
||||||
|
} else {
|
||||||
|
// Others, like IOException etc.
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
113
sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt
Normal file
113
sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
98
sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt
Normal file
98
sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,248 @@
|
||||||
|
// 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,224 @@
|
||||||
|
// 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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
// 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", arity = "0..*")
|
||||||
|
var withKeyPassword: List<String> = listOf()
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
val revokeKey = throwIfUnsupportedSubcommand(SopCLI.getSop().revokeKey(), "revoke-key")
|
||||||
|
|
||||||
|
if (!armor) {
|
||||||
|
revokeKey.noArmor()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (passwordIn in withKeyPassword) {
|
||||||
|
try {
|
||||||
|
val password = stringFromInputStream(getInput(passwordIn))
|
||||||
|
revokeKey.withKeyPassword(password)
|
||||||
|
} catch (e: SOPGPException.UnsupportedOption) {
|
||||||
|
val errorMsg =
|
||||||
|
getMsg("sop.error.feature_support.option_not_supported", "--with-key-password")
|
||||||
|
throw SOPGPException.UnsupportedOption(errorMsg, e)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val ready =
|
||||||
|
try {
|
||||||
|
revokeKey.keys(System.`in`)
|
||||||
|
} catch (e: KeyIsProtected) {
|
||||||
|
val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", "STANDARD_IN")
|
||||||
|
throw KeyIsProtected(errorMsg, e)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ready.writeTo(System.out)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
// 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,6 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
usage.header=Add ASCII Armor to standard input
|
usage.header=Add ASCII Armor to standard input
|
||||||
label=Label to be used in the header and tail of the armoring
|
|
||||||
|
|
||||||
stacktrace=Print stacktrace
|
stacktrace=Print stacktrace
|
||||||
# Generic TODO: Remove when bumping picocli to 4.7.0
|
# Generic TODO: Remove when bumping picocli to 4.7.0
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
usage.header=Schütze Standard-Eingabe mit ASCII Armor
|
usage.header=Schütze Standard-Eingabe mit ASCII Armor
|
||||||
label=Label für Kopf- und Fußzeile der ASCII Armor
|
|
||||||
|
|
||||||
stacktrace=Stacktrace ausgeben
|
stacktrace=Stacktrace ausgeben
|
||||||
# Generic TODO: Remove when bumping picocli to 4.7.0
|
# Generic TODO: Remove when bumping picocli to 4.7.0
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
sop.name=sop
|
sop.name=sop
|
||||||
|
sopv.name=sopv
|
||||||
usage.header=Stateless OpenPGP Protocol
|
usage.header=Stateless OpenPGP Protocol
|
||||||
|
sopv.usage.header=Stateless OpenPGP Protocol - Signature Verification Interface Subset
|
||||||
locale=Locale for description texts
|
locale=Locale for description texts
|
||||||
|
|
||||||
# Generic
|
# Generic
|
||||||
|
@ -34,6 +36,8 @@ usage.exitCodeList.17=73:Ambiguous input (a filename matching the designator alr
|
||||||
usage.exitCodeList.18=79:Key is not signing capable
|
usage.exitCodeList.18=79:Key is not signing capable
|
||||||
usage.exitCodeList.19=83:Options were supplied that are incompatible with each other
|
usage.exitCodeList.19=83:Options were supplied that are incompatible with each other
|
||||||
usage.exitCodeList.20=89:The requested profile is unsupported, or the indicated subcommand does not accept profiles
|
usage.exitCodeList.20=89:The requested profile is unsupported, or the indicated subcommand does not accept profiles
|
||||||
|
usage.exitCodeList.21=97:The implementation supports some form of hardware-backed secret keys, but could not identify the hardware device
|
||||||
|
usage.exitCodeList.22=101:The implementation tried to use a hardware-backed secret key, but the cryptographic hardware refused the operation for some reason other than a bad PIN or password
|
||||||
|
|
||||||
## SHARED RESOURCES
|
## SHARED RESOURCES
|
||||||
stacktrace=Print stacktrace
|
stacktrace=Print stacktrace
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
sop.name=sop
|
sop.name=sop
|
||||||
|
sopv.name=sopv
|
||||||
usage.header=Stateless OpenPGP Protocol
|
usage.header=Stateless OpenPGP Protocol
|
||||||
|
sopv.usage.header=Stateless OpenPGP Protocol - Signature Verification Interface Subset
|
||||||
locale=Gebietsschema für Beschreibungstexte
|
locale=Gebietsschema für Beschreibungstexte
|
||||||
|
|
||||||
# Generic
|
# Generic
|
||||||
|
@ -34,6 +36,8 @@ usage.exitCodeList.17=73:Mehrdeutige Eingabe (ein Dateiname, der dem Bezeichner
|
||||||
usage.exitCodeList.18=79:Schlüssel ist nicht fähig zu signieren
|
usage.exitCodeList.18=79:Schlüssel ist nicht fähig zu signieren
|
||||||
usage.exitCodeList.19=83:Miteinander inkompatible Optionen spezifiziert
|
usage.exitCodeList.19=83:Miteinander inkompatible Optionen spezifiziert
|
||||||
usage.exitCodeList.20=89:Das angeforderte Profil wird nicht unterstützt, oder der angegebene Unterbefehl akzeptiert keine Profile
|
usage.exitCodeList.20=89:Das angeforderte Profil wird nicht unterstützt, oder der angegebene Unterbefehl akzeptiert keine Profile
|
||||||
|
usage.exitCodeList.21=97:Die Anwendung unterstützt hardwaregestützte private Schlüssel, aber kann das Gerät nicht identifizieren
|
||||||
|
usage.exitCodeList.22=101:Die Anwendung versuchte, einen hardwaregestützten Schlüssel zu verwenden, aber das Gerät lehnte den Vorgang aus einem anderen Grund als einer falschen PIN oder einem falschen Passwort ab
|
||||||
|
|
||||||
## SHARED RESOURCES
|
## SHARED RESOURCES
|
||||||
stacktrace=Stacktrace ausgeben
|
stacktrace=Stacktrace ausgeben
|
||||||
|
|
|
@ -6,12 +6,13 @@ package sop.cli.picocli;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedSubcommand;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import sop.SOP;
|
import sop.SOP;
|
||||||
import sop.exception.SOPGPException;
|
import sop.exception.SOPGPException;
|
||||||
|
@ -34,20 +35,18 @@ import sop.operation.Version;
|
||||||
public class SOPTest {
|
public class SOPTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedSubcommand.EXIT_CODE)
|
|
||||||
public void assertExitOnInvalidSubcommand() {
|
public void assertExitOnInvalidSubcommand() {
|
||||||
SOP sop = mock(SOP.class);
|
SOP sop = mock(SOP.class);
|
||||||
SopCLI.setSopInstance(sop);
|
SopCLI.setSopInstance(sop);
|
||||||
|
|
||||||
SopCLI.main(new String[] {"invalid"});
|
assertUnsupportedSubcommand(() -> SopCLI.execute("invalid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(1)
|
|
||||||
public void assertThrowsIfNoSOPBackendSet() {
|
public void assertThrowsIfNoSOPBackendSet() {
|
||||||
SopCLI.SOP_INSTANCE = null;
|
SopCLI.setSopInstance(null);
|
||||||
// At this point, no SOP backend is set, so an InvalidStateException triggers exit(1)
|
// At this point, no SOP backend is set, so an InvalidStateException triggers error code 1
|
||||||
SopCLI.main(new String[] {"armor"});
|
assertGenericError(() -> SopCLI.execute("armor"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -36,7 +36,7 @@ public class AbstractSopCmdTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getInput_NullInvalid() {
|
public void getInput_NullInvalid() {
|
||||||
assertThrows(IllegalArgumentException.class, () -> abstractCmd.getInput(null));
|
assertThrows(NullPointerException.class, () -> abstractCmd.getInput(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -4,28 +4,27 @@
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.never;
|
|
||||||
import static org.mockito.Mockito.times;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
|
||||||
import com.ginsberg.junit.exit.FailOnSystemExit;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import sop.Ready;
|
import sop.Ready;
|
||||||
import sop.SOP;
|
import sop.SOP;
|
||||||
import sop.cli.picocli.SopCLI;
|
import sop.cli.picocli.SopCLI;
|
||||||
import sop.enums.ArmorLabel;
|
|
||||||
import sop.exception.SOPGPException;
|
import sop.exception.SOPGPException;
|
||||||
import sop.operation.Armor;
|
import sop.operation.Armor;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
|
||||||
|
|
||||||
public class ArmorCmdTest {
|
public class ArmorCmdTest {
|
||||||
|
|
||||||
private Armor armor;
|
private Armor armor;
|
||||||
|
@ -41,60 +40,30 @@ public class ArmorCmdTest {
|
||||||
SopCLI.setSopInstance(sop);
|
SopCLI.setSopInstance(sop);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void assertLabelIsNotCalledByDefault() throws SOPGPException.UnsupportedOption {
|
|
||||||
SopCLI.main(new String[] {"armor"});
|
|
||||||
verify(armor, never()).label(any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void assertLabelIsCalledWhenFlaggedWithArgument() throws SOPGPException.UnsupportedOption {
|
|
||||||
for (ArmorLabel label : ArmorLabel.values()) {
|
|
||||||
SopCLI.main(new String[] {"armor", "--label", label.name()});
|
|
||||||
verify(armor, times(1)).label(label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void assertDataIsAlwaysCalled() throws SOPGPException.BadData, IOException {
|
public void assertDataIsAlwaysCalled() throws SOPGPException.BadData, IOException {
|
||||||
SopCLI.main(new String[] {"armor"});
|
assertSuccess(() -> SopCLI.execute("armor"));
|
||||||
verify(armor, times(1)).data((InputStream) any());
|
verify(armor, times(1)).data((InputStream) any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public void assertThrowsForInvalidLabel() {
|
|
||||||
SopCLI.main(new String[] {"armor", "--label", "Invalid"});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public void ifLabelsUnsupportedExit37() throws SOPGPException.UnsupportedOption {
|
|
||||||
when(armor.label(any())).thenThrow(new SOPGPException.UnsupportedOption("Custom Armor labels are not supported."));
|
|
||||||
|
|
||||||
SopCLI.main(new String[] {"armor", "--label", "Sig"});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
|
|
||||||
public void ifBadDataExit41() throws SOPGPException.BadData, IOException {
|
public void ifBadDataExit41() throws SOPGPException.BadData, IOException {
|
||||||
when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
|
|
||||||
SopCLI.main(new String[] {"armor"});
|
assertBadData(() -> SopCLI.execute("armor"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@FailOnSystemExit
|
|
||||||
public void ifNoErrorsNoExit() {
|
public void ifNoErrorsNoExit() {
|
||||||
when(sop.armor()).thenReturn(armor);
|
when(sop.armor()).thenReturn(armor);
|
||||||
|
|
||||||
SopCLI.main(new String[] {"armor"});
|
assertSuccess(() -> SopCLI.execute("armor"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Ready nopReady() {
|
private static Ready nopReady() {
|
||||||
return new Ready() {
|
return new Ready() {
|
||||||
@Override
|
@Override
|
||||||
public void writeTo(OutputStream outputStream) {
|
public void writeTo(@Nonnull OutputStream outputStream) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,13 @@ import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import sop.Ready;
|
import sop.Ready;
|
||||||
|
@ -48,14 +49,13 @@ public class DearmorCmdTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void assertDataIsCalled() throws IOException, SOPGPException.BadData {
|
public void assertDataIsCalled() throws IOException, SOPGPException.BadData {
|
||||||
SopCLI.main(new String[] {"dearmor"});
|
assertSuccess(() -> SopCLI.execute("dearmor"));
|
||||||
verify(dearmor, times(1)).data((InputStream) any());
|
verify(dearmor, times(1)).data((InputStream) any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
|
|
||||||
public void assertBadDataCausesExit41() throws IOException, SOPGPException.BadData {
|
public void assertBadDataCausesExit41() throws IOException, SOPGPException.BadData {
|
||||||
when(dearmor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException("invalid armor")));
|
when(dearmor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException("invalid armor")));
|
||||||
SopCLI.main(new String[] {"dearmor"});
|
assertBadData(() -> SopCLI.execute("dearmor"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.ArgumentMatcher;
|
import org.mockito.ArgumentMatcher;
|
||||||
|
@ -21,6 +20,7 @@ import sop.operation.Decrypt;
|
||||||
import sop.util.HexUtil;
|
import sop.util.HexUtil;
|
||||||
import sop.util.UTCUtil;
|
import sop.util.UTCUtil;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -41,6 +41,18 @@ import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertCannotDecrypt;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertIncompleteVerification;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyIsProtected;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertOutputExists;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertPasswordNotHumanReadable;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedAsymmetricAlgo;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption;
|
||||||
|
|
||||||
public class DecryptCmdTest {
|
public class DecryptCmdTest {
|
||||||
|
|
||||||
|
@ -73,47 +85,47 @@ public class DecryptCmdTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE)
|
|
||||||
public void missingArgumentsExceptionCausesExit19() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException {
|
public void missingArgumentsExceptionCausesExit19() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException {
|
||||||
when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.MissingArg("Missing arguments."));
|
when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.MissingArg("Missing arguments."));
|
||||||
SopCLI.main(new String[] {"decrypt"});
|
assertMissingArg(() -> SopCLI.execute("decrypt"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
|
|
||||||
public void badDataExceptionCausesExit41() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException {
|
public void badDataExceptionCausesExit41() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException {
|
||||||
when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
SopCLI.main(new String[] {"decrypt"});
|
assertBadData(() -> SopCLI.execute("decrypt"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.PasswordNotHumanReadable.EXIT_CODE)
|
|
||||||
public void assertNotHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable,
|
public void assertNotHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable,
|
||||||
SOPGPException.UnsupportedOption, IOException {
|
SOPGPException.UnsupportedOption, IOException {
|
||||||
File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable");
|
File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable");
|
||||||
when(decrypt.withPassword(any())).thenThrow(new SOPGPException.PasswordNotHumanReadable());
|
when(decrypt.withPassword(any())).thenThrow(new SOPGPException.PasswordNotHumanReadable());
|
||||||
SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()});
|
assertPasswordNotHumanReadable(() ->
|
||||||
|
SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
|
public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
|
||||||
File passwordFile = TestFileUtil.writeTempStringFile("orange");
|
File passwordFile = TestFileUtil.writeTempStringFile("orange");
|
||||||
SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()});
|
assertSuccess(() -> SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath()));
|
||||||
verify(decrypt, times(1)).withPassword("orange");
|
verify(decrypt, times(1)).withPassword("orange");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
|
public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
|
||||||
File passwordFile = TestFileUtil.writeTempStringFile("swordfish");
|
File passwordFile = TestFileUtil.writeTempStringFile("swordfish");
|
||||||
when(decrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Decrypting with password not supported."));
|
when(decrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Decrypting with password not supported."));
|
||||||
SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()});
|
assertUnsupportedOption(() ->
|
||||||
|
SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption {
|
public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption {
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
SopCLI.main(new String[] {"decrypt"});
|
assertSuccess(() -> SopCLI.execute("decrypt"));
|
||||||
verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME);
|
verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME);
|
||||||
verify(decrypt, times(1)).verifyNotAfter(
|
verify(decrypt, times(1)).verifyNotAfter(
|
||||||
ArgumentMatchers.argThat(argument -> {
|
ArgumentMatchers.argThat(argument -> {
|
||||||
|
@ -124,7 +136,8 @@ public class DecryptCmdTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption {
|
public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption {
|
||||||
SopCLI.main(new String[] {"decrypt", "--verify-not-before", "-", "--verify-not-after", "-"});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("decrypt", "--verify-not-before", "-", "--verify-not-after", "-"));
|
||||||
verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME);
|
verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME);
|
||||||
verify(decrypt, times(1)).verifyNotAfter(AbstractSopCmd.END_OF_TIME);
|
verify(decrypt, times(1)).verifyNotAfter(AbstractSopCmd.END_OF_TIME);
|
||||||
}
|
}
|
||||||
|
@ -137,54 +150,57 @@ public class DecryptCmdTest {
|
||||||
return Math.abs(now.getTime() - argument.getTime()) <= 1000;
|
return Math.abs(now.getTime() - argument.getTime()) <= 1000;
|
||||||
};
|
};
|
||||||
|
|
||||||
SopCLI.main(new String[] {"decrypt", "--verify-not-before", "now", "--verify-not-after", "now"});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("decrypt", "--verify-not-before", "now", "--verify-not-after", "now"));
|
||||||
verify(decrypt, times(1)).verifyNotAfter(ArgumentMatchers.argThat(isMaxOneSecOff));
|
verify(decrypt, times(1)).verifyNotAfter(ArgumentMatchers.argThat(isMaxOneSecOff));
|
||||||
verify(decrypt, times(1)).verifyNotBefore(ArgumentMatchers.argThat(isMaxOneSecOff));
|
verify(decrypt, times(1)).verifyNotBefore(ArgumentMatchers.argThat(isMaxOneSecOff));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(1)
|
|
||||||
public void assertMalformedDateInNotBeforeCausesExit1() {
|
public void assertMalformedDateInNotBeforeCausesExit1() {
|
||||||
// ParserException causes exit(1)
|
// ParserException causes exit(1)
|
||||||
SopCLI.main(new String[] {"decrypt", "--verify-not-before", "invalid"});
|
assertGenericError(() ->
|
||||||
|
SopCLI.execute("decrypt", "--verify-not-before", "invalid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(1)
|
|
||||||
public void assertMalformedDateInNotAfterCausesExit1() {
|
public void assertMalformedDateInNotAfterCausesExit1() {
|
||||||
// ParserException causes exit(1)
|
// ParserException causes exit(1)
|
||||||
SopCLI.main(new String[] {"decrypt", "--verify-not-after", "invalid"});
|
assertGenericError(() ->
|
||||||
|
SopCLI.execute("decrypt", "--verify-not-after", "invalid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public void assertUnsupportedNotAfterCausesExit37() throws SOPGPException.UnsupportedOption {
|
public void assertUnsupportedNotAfterCausesExit37() throws SOPGPException.UnsupportedOption {
|
||||||
when(decrypt.verifyNotAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported."));
|
when(decrypt.verifyNotAfter(any())).thenThrow(
|
||||||
SopCLI.main(new String[] {"decrypt", "--verify-not-after", "now"});
|
new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported."));
|
||||||
|
assertUnsupportedOption(() ->
|
||||||
|
SopCLI.execute("decrypt", "--verify-not-after", "now"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public void assertUnsupportedNotBeforeCausesExit37() throws SOPGPException.UnsupportedOption {
|
public void assertUnsupportedNotBeforeCausesExit37() throws SOPGPException.UnsupportedOption {
|
||||||
when(decrypt.verifyNotBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported."));
|
when(decrypt.verifyNotBefore(any())).thenThrow(
|
||||||
SopCLI.main(new String[] {"decrypt", "--verify-not-before", "now"});
|
new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported."));
|
||||||
|
assertUnsupportedOption(() ->
|
||||||
|
SopCLI.execute("decrypt", "--verify-not-before", "now"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE)
|
|
||||||
public void assertExistingSessionKeyOutFileCausesExit59() throws IOException {
|
public void assertExistingSessionKeyOutFileCausesExit59() throws IOException {
|
||||||
File tempFile = File.createTempFile("existing-session-key-", ".tmp");
|
File tempFile = File.createTempFile("existing-session-key-", ".tmp");
|
||||||
tempFile.deleteOnExit();
|
tempFile.deleteOnExit();
|
||||||
SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()});
|
assertOutputExists(() ->
|
||||||
|
SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public void assertWhenSessionKeyCannotBeExtractedExit37() throws IOException {
|
public void assertWhenSessionKeyCannotBeExtractedExit37() throws IOException {
|
||||||
Path tempDir = Files.createTempDirectory("session-key-out-dir");
|
Path tempDir = Files.createTempDirectory("session-key-out-dir");
|
||||||
File tempFile = new File(tempDir.toFile(), "session-key");
|
File tempFile = new File(tempDir.toFile(), "session-key");
|
||||||
tempFile.deleteOnExit();
|
tempFile.deleteOnExit();
|
||||||
SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()});
|
assertUnsupportedOption(() ->
|
||||||
|
SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -209,8 +225,10 @@ public class DecryptCmdTest {
|
||||||
File verificationsFile = new File(tempDir.toFile(), "verifications");
|
File verificationsFile = new File(tempDir.toFile(), "verifications");
|
||||||
File keyFile = new File(tempDir.toFile(), "key.asc");
|
File keyFile = new File(tempDir.toFile(), "key.asc");
|
||||||
keyFile.createNewFile();
|
keyFile.createNewFile();
|
||||||
SopCLI.main(new String[] {"decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(),
|
assertSuccess(() ->
|
||||||
"--verifications-out", verificationsFile.getAbsolutePath(), "--verify-with", keyFile.getAbsolutePath()});
|
SopCLI.execute("decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(),
|
||||||
|
"--verifications-out", verificationsFile.getAbsolutePath(), "--verify-with",
|
||||||
|
keyFile.getAbsolutePath()));
|
||||||
|
|
||||||
ByteArrayOutputStream bytesInFile = new ByteArrayOutputStream();
|
ByteArrayOutputStream bytesInFile = new ByteArrayOutputStream();
|
||||||
try (FileInputStream fileIn = new FileInputStream(sessionKeyFile)) {
|
try (FileInputStream fileIn = new FileInputStream(sessionKeyFile)) {
|
||||||
|
@ -240,45 +258,49 @@ public class DecryptCmdTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.CannotDecrypt.EXIT_CODE)
|
|
||||||
public void assertUnableToDecryptExceptionResultsInExit29() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException {
|
public void assertUnableToDecryptExceptionResultsInExit29() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException {
|
||||||
when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.CannotDecrypt());
|
when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.CannotDecrypt());
|
||||||
SopCLI.main(new String[] {"decrypt"});
|
assertCannotDecrypt(() ->
|
||||||
|
SopCLI.execute("decrypt"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.NoSignature.EXIT_CODE)
|
public void assertNoVerificationsIsOkay() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException {
|
||||||
public void assertNoSignatureExceptionCausesExit3() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException {
|
File tempFile = File.createTempFile("verify-with-", ".tmp");
|
||||||
|
File verifyOut = new File(tempFile.getParent(), "verifications.out");
|
||||||
|
verifyOut.deleteOnExit();
|
||||||
when(decrypt.ciphertext((InputStream) any())).thenReturn(new ReadyWithResult<DecryptionResult>() {
|
when(decrypt.ciphertext((InputStream) any())).thenReturn(new ReadyWithResult<DecryptionResult>() {
|
||||||
@Override
|
@Override
|
||||||
public DecryptionResult writeTo(OutputStream outputStream) throws SOPGPException.NoSignature {
|
public DecryptionResult writeTo(@Nonnull OutputStream outputStream) throws SOPGPException.NoSignature {
|
||||||
throw new SOPGPException.NoSignature();
|
return new DecryptionResult(null, Collections.emptyList());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
SopCLI.main(new String[] {"decrypt"});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("decrypt", "--verify-with", tempFile.getAbsolutePath(), "--verifications-out",
|
||||||
|
verifyOut.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
|
|
||||||
public void badDataInVerifyWithCausesExit41() throws IOException, SOPGPException.BadData {
|
public void badDataInVerifyWithCausesExit41() throws IOException, SOPGPException.BadData {
|
||||||
when(decrypt.verifyWithCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
when(decrypt.verifyWithCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
File tempFile = File.createTempFile("verify-with-", ".tmp");
|
File tempFile = File.createTempFile("verify-with-", ".tmp");
|
||||||
SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath()});
|
assertBadData(() ->
|
||||||
|
SopCLI.execute("decrypt", "--verify-with", tempFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
|
|
||||||
public void unexistentCertFileCausesExit61() {
|
public void unexistentCertFileCausesExit61() {
|
||||||
SopCLI.main(new String[] {"decrypt", "--verify-with", "invalid"});
|
assertMissingInput(() ->
|
||||||
|
SopCLI.execute("decrypt", "--verify-with", "invalid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE)
|
|
||||||
public void existingVerifyOutCausesExit59() throws IOException {
|
public void existingVerifyOutCausesExit59() throws IOException {
|
||||||
File certFile = File.createTempFile("existing-verify-out-cert", ".asc");
|
File certFile = File.createTempFile("existing-verify-out-cert", ".asc");
|
||||||
File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp");
|
File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp");
|
||||||
|
|
||||||
SopCLI.main(new String[] {"decrypt", "--verify-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()});
|
assertOutputExists(() -> SopCLI.execute("decrypt", "--verifications-out",
|
||||||
|
existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -302,7 +324,9 @@ public class DecryptCmdTest {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
SopCLI.main(new String[] {"decrypt", "--verify-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("decrypt", "--verifications-out", verifyOut.getAbsolutePath(),
|
||||||
|
"--verify-with", certFile.getAbsolutePath()));
|
||||||
try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) {
|
try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) {
|
||||||
String line = reader.readLine();
|
String line = reader.readLine();
|
||||||
assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line);
|
assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line);
|
||||||
|
@ -317,66 +341,64 @@ public class DecryptCmdTest {
|
||||||
File sessionKeyFile1 = TestFileUtil.writeTempStringFile(key1.toString());
|
File sessionKeyFile1 = TestFileUtil.writeTempStringFile(key1.toString());
|
||||||
File sessionKeyFile2 = TestFileUtil.writeTempStringFile(key2.toString());
|
File sessionKeyFile2 = TestFileUtil.writeTempStringFile(key2.toString());
|
||||||
|
|
||||||
SopCLI.main(new String[] {"decrypt",
|
assertSuccess(() ->
|
||||||
"--with-session-key", sessionKeyFile1.getAbsolutePath(),
|
SopCLI.execute("decrypt",
|
||||||
"--with-session-key", sessionKeyFile2.getAbsolutePath()});
|
"--with-session-key", sessionKeyFile1.getAbsolutePath(),
|
||||||
|
"--with-session-key", sessionKeyFile2.getAbsolutePath()));
|
||||||
verify(decrypt).withSessionKey(key1);
|
verify(decrypt).withSessionKey(key1);
|
||||||
verify(decrypt).withSessionKey(key2);
|
verify(decrypt).withSessionKey(key2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(1)
|
|
||||||
public void assertMalformedSessionKeysResultInExit1() throws IOException {
|
public void assertMalformedSessionKeysResultInExit1() throws IOException {
|
||||||
File sessionKeyFile = TestFileUtil.writeTempStringFile("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137");
|
File sessionKeyFile = TestFileUtil.writeTempStringFile("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137");
|
||||||
SopCLI.main(new String[] {"decrypt",
|
assertGenericError(() ->
|
||||||
"--with-session-key", sessionKeyFile.getAbsolutePath()});
|
SopCLI.execute("decrypt",
|
||||||
|
"--with-session-key", sessionKeyFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
|
|
||||||
public void assertBadDataInKeysResultsInExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException {
|
public void assertBadDataInKeysResultsInExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException {
|
||||||
when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
File tempKeyFile = File.createTempFile("key-", ".tmp");
|
File tempKeyFile = File.createTempFile("key-", ".tmp");
|
||||||
SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()});
|
assertBadData(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
|
|
||||||
public void assertKeyFileNotFoundCausesExit61() {
|
public void assertKeyFileNotFoundCausesExit61() {
|
||||||
SopCLI.main(new String[] {"decrypt", "nonexistent-key"});
|
assertMissingInput(() -> SopCLI.execute("decrypt", "nonexistent-key"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE)
|
|
||||||
public void assertProtectedKeyCausesExit67() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
|
public void assertProtectedKeyCausesExit67() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
|
||||||
when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
|
when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
|
||||||
File tempKeyFile = File.createTempFile("key-", ".tmp");
|
File tempKeyFile = File.createTempFile("key-", ".tmp");
|
||||||
SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()});
|
assertKeyIsProtected(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE)
|
|
||||||
public void assertUnsupportedAlgorithmExceptionCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException {
|
public void assertUnsupportedAlgorithmExceptionCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException {
|
||||||
when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new IOException()));
|
when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new IOException()));
|
||||||
File tempKeyFile = File.createTempFile("key-", ".tmp");
|
File tempKeyFile = File.createTempFile("key-", ".tmp");
|
||||||
SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()});
|
assertUnsupportedAsymmetricAlgo(() ->
|
||||||
|
SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
|
|
||||||
public void assertMissingPassphraseFileCausesExit61() {
|
public void assertMissingPassphraseFileCausesExit61() {
|
||||||
SopCLI.main(new String[] {"decrypt", "--with-password", "missing"});
|
assertMissingInput(() ->
|
||||||
|
SopCLI.execute("decrypt", "--with-password", "missing"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
|
|
||||||
public void assertMissingSessionKeyFileCausesExit61() {
|
public void assertMissingSessionKeyFileCausesExit61() {
|
||||||
SopCLI.main(new String[] {"decrypt", "--with-session-key", "missing"});
|
assertMissingInput(() ->
|
||||||
|
SopCLI.execute("decrypt", "--with-session-key", "missing"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.IncompleteVerification.EXIT_CODE)
|
|
||||||
public void verifyOutWithoutVerifyWithCausesExit23() {
|
public void verifyOutWithoutVerifyWithCausesExit23() {
|
||||||
SopCLI.main(new String[] {"decrypt", "--verify-out", "out.file"});
|
assertIncompleteVerification(() ->
|
||||||
|
SopCLI.execute("decrypt", "--verifications-out", "out.file"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,28 +4,40 @@
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import sop.EncryptionResult;
|
||||||
|
import sop.ReadyWithResult;
|
||||||
|
import sop.SOP;
|
||||||
|
import sop.cli.picocli.SopCLI;
|
||||||
|
import sop.cli.picocli.TestFileUtil;
|
||||||
|
import sop.enums.EncryptAs;
|
||||||
|
import sop.exception.SOPGPException;
|
||||||
|
import sop.operation.Encrypt;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
|
||||||
import java.io.File;
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertCertCannotEncrypt;
|
||||||
import java.io.IOException;
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError;
|
||||||
import java.io.InputStream;
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyCannotSign;
|
||||||
import java.io.OutputStream;
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyIsProtected;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg;
|
||||||
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertPasswordNotHumanReadable;
|
||||||
import org.junit.jupiter.api.Test;
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
|
||||||
import sop.Ready;
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedAsymmetricAlgo;
|
||||||
import sop.SOP;
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption;
|
||||||
import sop.cli.picocli.SopCLI;
|
|
||||||
import sop.cli.picocli.TestFileUtil;
|
|
||||||
import sop.enums.EncryptAs;
|
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.Encrypt;
|
|
||||||
|
|
||||||
public class EncryptCmdTest {
|
public class EncryptCmdTest {
|
||||||
|
|
||||||
|
@ -34,10 +46,10 @@ public class EncryptCmdTest {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void mockComponents() throws IOException {
|
public void mockComponents() throws IOException {
|
||||||
encrypt = mock(Encrypt.class);
|
encrypt = mock(Encrypt.class);
|
||||||
when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() {
|
when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult<EncryptionResult>() {
|
||||||
@Override
|
@Override
|
||||||
public void writeTo(OutputStream outputStream) {
|
public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException {
|
||||||
|
return new EncryptionResult(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -48,48 +60,50 @@ public class EncryptCmdTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE)
|
public void missingBothPasswordAndCertFileCausesMissingArg() {
|
||||||
public void missingBothPasswordAndCertFileCauseExit19() {
|
assertMissingArg(() ->
|
||||||
SopCLI.main(new String[] {"encrypt", "--no-armor"});
|
SopCLI.execute("encrypt", "--no-armor"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
|
public void as_unsupportedEncryptAsCausesUnsupportedOption() throws SOPGPException.UnsupportedOption {
|
||||||
public void as_unsupportedEncryptAsCausesExit37() throws SOPGPException.UnsupportedOption {
|
|
||||||
when(encrypt.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting encryption mode not supported."));
|
when(encrypt.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting encryption mode not supported."));
|
||||||
|
|
||||||
SopCLI.main(new String[] {"encrypt", "--as", "Binary"});
|
assertUnsupportedOption(() ->
|
||||||
|
SopCLI.execute("encrypt", "--as", "Binary"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
|
public void as_invalidModeOptionCausesUnsupportedOption() {
|
||||||
public void as_invalidModeOptionCausesExit37() {
|
assertUnsupportedOption(() ->
|
||||||
SopCLI.main(new String[] {"encrypt", "--as", "invalid"});
|
SopCLI.execute("encrypt", "--as", "invalid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption, IOException {
|
public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption, IOException {
|
||||||
File passwordFile = TestFileUtil.writeTempStringFile("0rbit");
|
File passwordFile = TestFileUtil.writeTempStringFile("0rbit");
|
||||||
for (EncryptAs mode : EncryptAs.values()) {
|
for (EncryptAs mode : EncryptAs.values()) {
|
||||||
SopCLI.main(new String[] {"encrypt", "--as", mode.name(), "--with-password", passwordFile.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("encrypt", "--as", mode.name(),
|
||||||
|
"--with-password", passwordFile.getAbsolutePath()));
|
||||||
verify(encrypt, times(1)).mode(mode);
|
verify(encrypt, times(1)).mode(mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.PasswordNotHumanReadable.EXIT_CODE)
|
public void withPassword_notHumanReadablePasswordCausesPWNotHumanReadable() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
|
||||||
public void withPassword_notHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
|
|
||||||
when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable());
|
when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable());
|
||||||
File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable");
|
File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable");
|
||||||
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()});
|
assertPasswordNotHumanReadable(() ->
|
||||||
|
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
|
public void withPassword_unsupportedWithPasswordCausesUnsupportedOption() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
|
||||||
public void withPassword_unsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
|
|
||||||
when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported."));
|
when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported."));
|
||||||
File passwordFile = TestFileUtil.writeTempStringFile("orange");
|
File passwordFile = TestFileUtil.writeTempStringFile("orange");
|
||||||
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()});
|
assertUnsupportedOption(() ->
|
||||||
|
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -97,106 +111,115 @@ public class EncryptCmdTest {
|
||||||
File keyFile1 = File.createTempFile("sign-with-1-", ".asc");
|
File keyFile1 = File.createTempFile("sign-with-1-", ".asc");
|
||||||
File keyFile2 = File.createTempFile("sign-with-2-", ".asc");
|
File keyFile2 = File.createTempFile("sign-with-2-", ".asc");
|
||||||
File passwordFile = TestFileUtil.writeTempStringFile("password");
|
File passwordFile = TestFileUtil.writeTempStringFile("password");
|
||||||
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile1.getAbsolutePath(), "--sign-with", keyFile2.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(),
|
||||||
|
"--sign-with", keyFile1.getAbsolutePath(),
|
||||||
|
"--sign-with", keyFile2.getAbsolutePath()));
|
||||||
verify(encrypt, times(2)).signWith((InputStream) any());
|
verify(encrypt, times(2)).signWith((InputStream) any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
|
public void signWith_nonExistentKeyFileCausesMissingInput() {
|
||||||
public void signWith_nonExistentKeyFileCausesExit61() {
|
assertMissingInput(() ->
|
||||||
SopCLI.main(new String[] {"encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"});
|
SopCLI.execute("encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE)
|
public void signWith_keyIsProtectedCausesKeyIsProtected() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
|
||||||
public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
|
|
||||||
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
|
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
|
||||||
File keyFile = File.createTempFile("sign-with", ".asc");
|
File keyFile = File.createTempFile("sign-with", ".asc");
|
||||||
File passwordFile = TestFileUtil.writeTempStringFile("starship");
|
File passwordFile = TestFileUtil.writeTempStringFile("starship");
|
||||||
SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", passwordFile.getAbsolutePath()});
|
assertKeyIsProtected(() ->
|
||||||
|
SopCLI.execute("encrypt", "--sign-with", keyFile.getAbsolutePath(),
|
||||||
|
"--with-password", passwordFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE)
|
public void signWith_unsupportedAsymmetricAlgoCausesUnsupportedAsymAlgo() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
|
||||||
public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
|
|
||||||
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception()));
|
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception()));
|
||||||
File keyFile = File.createTempFile("sign-with", ".asc");
|
File keyFile = File.createTempFile("sign-with", ".asc");
|
||||||
File passwordFile = TestFileUtil.writeTempStringFile("123456");
|
File passwordFile = TestFileUtil.writeTempStringFile("123456");
|
||||||
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()});
|
assertUnsupportedAsymmetricAlgo(() ->
|
||||||
|
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(),
|
||||||
|
"--sign-with", keyFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.KeyCannotSign.EXIT_CODE)
|
public void signWith_certCannotSignCausesKeyCannotSign() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData {
|
||||||
public void signWith_certCannotSignCausesExit79() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData {
|
|
||||||
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign());
|
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign());
|
||||||
File keyFile = File.createTempFile("sign-with", ".asc");
|
File keyFile = File.createTempFile("sign-with", ".asc");
|
||||||
File passwordFile = TestFileUtil.writeTempStringFile("dragon");
|
File passwordFile = TestFileUtil.writeTempStringFile("dragon");
|
||||||
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()});
|
assertKeyCannotSign(() ->
|
||||||
|
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(),
|
||||||
|
"--sign-with", keyFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
|
public void signWith_badDataCausesBadData() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
|
||||||
public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
|
|
||||||
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
File keyFile = File.createTempFile("sign-with", ".asc");
|
File keyFile = File.createTempFile("sign-with", ".asc");
|
||||||
File passwordFile = TestFileUtil.writeTempStringFile("orange");
|
File passwordFile = TestFileUtil.writeTempStringFile("orange");
|
||||||
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()});
|
assertBadData(() ->
|
||||||
|
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(),
|
||||||
|
"--sign-with", keyFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
|
public void cert_nonExistentCertFileCausesMissingInput() {
|
||||||
public void cert_nonExistentCertFileCausesExit61() {
|
assertMissingInput(() ->
|
||||||
SopCLI.main(new String[] {"encrypt", "invalid.asc"});
|
SopCLI.execute("encrypt", "invalid.asc"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE)
|
public void cert_unsupportedAsymmetricAlgorithmCausesUnsupportedAsymAlg() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
|
||||||
public void cert_unsupportedAsymmetricAlgorithmCausesExit13() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
|
|
||||||
when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception()));
|
when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception()));
|
||||||
File certFile = File.createTempFile("cert", ".asc");
|
File certFile = File.createTempFile("cert", ".asc");
|
||||||
SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()});
|
assertUnsupportedAsymmetricAlgo(() ->
|
||||||
|
SopCLI.execute("encrypt", certFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.CertCannotEncrypt.EXIT_CODE)
|
public void cert_certCannotEncryptCausesCertCannotEncrypt() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
|
||||||
public void cert_certCannotEncryptCausesExit17() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
|
|
||||||
when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.CertCannotEncrypt("Certificate cannot encrypt.", new Exception()));
|
when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.CertCannotEncrypt("Certificate cannot encrypt.", new Exception()));
|
||||||
File certFile = File.createTempFile("cert", ".asc");
|
File certFile = File.createTempFile("cert", ".asc");
|
||||||
SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()});
|
assertCertCannotEncrypt(() ->
|
||||||
|
SopCLI.execute("encrypt", certFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
|
public void cert_badDataCausesBadData() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
|
||||||
public void cert_badDataCausesExit41() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
|
|
||||||
when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
File certFile = File.createTempFile("cert", ".asc");
|
File certFile = File.createTempFile("cert", ".asc");
|
||||||
SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()});
|
assertBadData(() ->
|
||||||
|
SopCLI.execute("encrypt", certFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void noArmor_notCalledByDefault() throws IOException {
|
public void noArmor_notCalledByDefault() throws IOException {
|
||||||
File passwordFile = TestFileUtil.writeTempStringFile("clownfish");
|
File passwordFile = TestFileUtil.writeTempStringFile("clownfish");
|
||||||
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath()));
|
||||||
verify(encrypt, never()).noArmor();
|
verify(encrypt, never()).noArmor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void noArmor_callGetsPassedDown() throws IOException {
|
public void noArmor_callGetsPassedDown() throws IOException {
|
||||||
File passwordFile = TestFileUtil.writeTempStringFile("monkey");
|
File passwordFile = TestFileUtil.writeTempStringFile("monkey");
|
||||||
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--no-armor"});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), "--no-armor"));
|
||||||
verify(encrypt, times(1)).noArmor();
|
verify(encrypt, times(1)).noArmor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(1)
|
public void writeTo_ioExceptionCausesGenericError() throws IOException {
|
||||||
public void writeTo_ioExceptionCausesExit1() throws IOException {
|
when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult<EncryptionResult>() {
|
||||||
when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() {
|
|
||||||
@Override
|
@Override
|
||||||
public void writeTo(OutputStream outputStream) throws IOException {
|
public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException {
|
||||||
throw new IOException();
|
throw new IOException();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
File passwordFile = TestFileUtil.writeTempStringFile("wildcat");
|
File passwordFile = TestFileUtil.writeTempStringFile("wildcat");
|
||||||
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()});
|
assertGenericError(() ->
|
||||||
|
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,14 @@ import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import sop.Ready;
|
import sop.Ready;
|
||||||
|
@ -45,32 +47,34 @@ public class ExtractCertCmdTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void noArmor_notCalledByDefault() {
|
public void noArmor_notCalledByDefault() {
|
||||||
SopCLI.main(new String[] {"extract-cert"});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("extract-cert"));
|
||||||
verify(extractCert, never()).noArmor();
|
verify(extractCert, never()).noArmor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void noArmor_passedDown() {
|
public void noArmor_passedDown() {
|
||||||
SopCLI.main(new String[] {"extract-cert", "--no-armor"});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("extract-cert", "--no-armor"));
|
||||||
verify(extractCert, times(1)).noArmor();
|
verify(extractCert, times(1)).noArmor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(1)
|
public void key_ioExceptionCausesGenericError() throws IOException, SOPGPException.BadData {
|
||||||
public void key_ioExceptionCausesExit1() throws IOException, SOPGPException.BadData {
|
|
||||||
when(extractCert.key((InputStream) any())).thenReturn(new Ready() {
|
when(extractCert.key((InputStream) any())).thenReturn(new Ready() {
|
||||||
@Override
|
@Override
|
||||||
public void writeTo(OutputStream outputStream) throws IOException {
|
public void writeTo(OutputStream outputStream) throws IOException {
|
||||||
throw new IOException();
|
throw new IOException();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
SopCLI.main(new String[] {"extract-cert"});
|
assertGenericError(() ->
|
||||||
|
SopCLI.execute("extract-cert"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
|
public void key_badDataCausesBadData() throws IOException, SOPGPException.BadData {
|
||||||
public void key_badDataCausesExit41() throws IOException, SOPGPException.BadData {
|
|
||||||
when(extractCert.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
when(extractCert.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
SopCLI.main(new String[] {"extract-cert"});
|
assertBadData(() ->
|
||||||
|
SopCLI.execute("extract-cert"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,14 @@ import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedAsymmetricAlgo;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.InOrder;
|
import org.mockito.InOrder;
|
||||||
|
@ -47,19 +50,22 @@ public class GenerateKeyCmdTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void noArmor_notCalledByDefault() {
|
public void noArmor_notCalledByDefault() {
|
||||||
SopCLI.main(new String[] {"generate-key", "Alice"});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("generate-key", "Alice"));
|
||||||
verify(generateKey, never()).noArmor();
|
verify(generateKey, never()).noArmor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void noArmor_passedDown() {
|
public void noArmor_passedDown() {
|
||||||
SopCLI.main(new String[] {"generate-key", "--no-armor", "Alice"});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("generate-key", "--no-armor", "Alice"));
|
||||||
verify(generateKey, times(1)).noArmor();
|
verify(generateKey, times(1)).noArmor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void userId_multipleUserIdsPassedDownInProperOrder() {
|
public void userId_multipleUserIdsPassedDownInProperOrder() {
|
||||||
SopCLI.main(new String[] {"generate-key", "Alice <alice@pgpainless.org>", "Bob <bob@pgpainless.org>"});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("generate-key", "Alice <alice@pgpainless.org>", "Bob <bob@pgpainless.org>"));
|
||||||
|
|
||||||
InOrder inOrder = Mockito.inOrder(generateKey);
|
InOrder inOrder = Mockito.inOrder(generateKey);
|
||||||
inOrder.verify(generateKey).userId("Alice <alice@pgpainless.org>");
|
inOrder.verify(generateKey).userId("Alice <alice@pgpainless.org>");
|
||||||
|
@ -69,30 +75,32 @@ public class GenerateKeyCmdTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE)
|
|
||||||
public void missingArgumentCausesExit19() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
|
public void missingArgumentCausesExit19() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
|
||||||
// TODO: RFC4880-bis and the current Stateless OpenPGP CLI spec allow keys to have no user-ids,
|
// TODO: RFC4880-bis and the current Stateless OpenPGP CLI spec allow keys to have no user-ids,
|
||||||
// so we might want to change this test in the future.
|
// so we might want to change this test in the future.
|
||||||
when(generateKey.generate()).thenThrow(new SOPGPException.MissingArg("Missing user-id."));
|
when(generateKey.generate()).thenThrow(new SOPGPException.MissingArg("Missing user-id."));
|
||||||
SopCLI.main(new String[] {"generate-key"});
|
assertMissingArg(() ->
|
||||||
|
SopCLI.execute("generate-key"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE)
|
|
||||||
public void unsupportedAsymmetricAlgorithmCausesExit13() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
|
public void unsupportedAsymmetricAlgorithmCausesExit13() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
|
||||||
when(generateKey.generate()).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception()));
|
when(generateKey.generate()).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception()));
|
||||||
SopCLI.main(new String[] {"generate-key", "Alice"});
|
assertUnsupportedAsymmetricAlgo(() ->
|
||||||
|
SopCLI.execute("generate-key", "Alice"));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(1)
|
public void ioExceptionCausesGenericError() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
|
||||||
public void ioExceptionCausesExit1() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
|
|
||||||
when(generateKey.generate()).thenReturn(new Ready() {
|
when(generateKey.generate()).thenReturn(new Ready() {
|
||||||
@Override
|
@Override
|
||||||
public void writeTo(OutputStream outputStream) throws IOException {
|
public void writeTo(OutputStream outputStream) throws IOException {
|
||||||
throw new IOException();
|
throw new IOException();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
SopCLI.main(new String[] {"generate-key", "Alice"});
|
|
||||||
|
assertGenericError(() ->
|
||||||
|
SopCLI.execute("generate-key", "Alice"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import sop.ReadyWithResult;
|
import sop.ReadyWithResult;
|
||||||
|
@ -26,6 +25,8 @@ import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
|
||||||
|
|
||||||
public class InlineDetachCmdTest {
|
public class InlineDetachCmdTest {
|
||||||
|
|
||||||
|
@ -41,9 +42,9 @@ public class InlineDetachCmdTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE)
|
public void testMissingSignaturesOutResultsInMissingArg() {
|
||||||
public void testMissingSignaturesOutResultsInExit19() {
|
assertMissingArg(() ->
|
||||||
SopCLI.main(new String[] {"inline-detach"});
|
SopCLI.execute("inline-detach"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -67,7 +68,8 @@ public class InlineDetachCmdTest {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
SopCLI.main(new String[] {"inline-detach", "--signatures-out", tempFile.getAbsolutePath(), "--no-armor"});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("inline-detach", "--signatures-out", tempFile.getAbsolutePath(), "--no-armor"));
|
||||||
verify(inlineDetach, times(1)).noArmor();
|
verify(inlineDetach, times(1)).noArmor();
|
||||||
verify(inlineDetach, times(1)).message((InputStream) any());
|
verify(inlineDetach, times(1)).message((InputStream) any());
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,20 @@ import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertExpectedText;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyIsProtected;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import sop.ReadyWithResult;
|
import sop.ReadyWithResult;
|
||||||
|
@ -54,70 +61,77 @@ public class SignCmdTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void as_optionsAreCaseInsensitive() {
|
public void as_optionsAreCaseInsensitive() {
|
||||||
SopCLI.main(new String[] {"sign", "--as", "Binary", keyFile.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()});
|
SopCLI.execute("sign", "--as", "Binary", keyFile.getAbsolutePath()));
|
||||||
SopCLI.main(new String[] {"sign", "--as", "BINARY", keyFile.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("sign", "--as", "binary", keyFile.getAbsolutePath()));
|
||||||
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("sign", "--as", "BINARY", keyFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public void as_invalidOptionCausesExit37() {
|
public void as_invalidOptionCausesExit37() {
|
||||||
SopCLI.main(new String[] {"sign", "--as", "Invalid", keyFile.getAbsolutePath()});
|
assertUnsupportedOption(() ->
|
||||||
|
SopCLI.execute("sign", "--as", "Invalid", keyFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
|
public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
|
||||||
when(detachedSign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting signing mode not supported."));
|
when(detachedSign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting signing mode not supported."));
|
||||||
SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()});
|
assertUnsupportedOption(() ->
|
||||||
|
SopCLI.execute("sign", "--as", "binary", keyFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
|
|
||||||
public void key_nonExistentKeyFileCausesExit61() {
|
public void key_nonExistentKeyFileCausesExit61() {
|
||||||
SopCLI.main(new String[] {"sign", "invalid.asc"});
|
assertMissingInput(() ->
|
||||||
|
SopCLI.execute("sign", "invalid.asc"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE)
|
|
||||||
public void key_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData {
|
public void key_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData {
|
||||||
when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
|
when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
|
||||||
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
|
assertKeyIsProtected(() ->
|
||||||
|
SopCLI.execute("sign", keyFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
|
|
||||||
public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData {
|
public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData {
|
||||||
when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
|
assertBadData(() ->
|
||||||
|
SopCLI.execute("sign", keyFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE)
|
|
||||||
public void key_missingKeyFileCausesExit19() {
|
public void key_missingKeyFileCausesExit19() {
|
||||||
SopCLI.main(new String[] {"sign"});
|
assertMissingArg(() ->
|
||||||
|
SopCLI.execute("sign"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void noArmor_notCalledByDefault() {
|
public void noArmor_notCalledByDefault() {
|
||||||
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("sign", keyFile.getAbsolutePath()));
|
||||||
verify(detachedSign, never()).noArmor();
|
verify(detachedSign, never()).noArmor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void noArmor_passedDown() {
|
public void noArmor_passedDown() {
|
||||||
SopCLI.main(new String[] {"sign", "--no-armor", keyFile.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("sign", "--no-armor", keyFile.getAbsolutePath()));
|
||||||
verify(detachedSign, times(1)).noArmor();
|
verify(detachedSign, times(1)).noArmor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void withKeyPassword_passedDown() {
|
public void withKeyPassword_passedDown() {
|
||||||
SopCLI.main(new String[] {"sign", "--with-key-password", passFile.getAbsolutePath(), keyFile.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("sign",
|
||||||
|
"--with-key-password", passFile.getAbsolutePath(),
|
||||||
|
keyFile.getAbsolutePath()));
|
||||||
verify(detachedSign, times(1)).withKeyPassword("sw0rdf1sh");
|
verify(detachedSign, times(1)).withKeyPassword("sw0rdf1sh");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(1)
|
|
||||||
public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText {
|
public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText {
|
||||||
when(detachedSign.data((InputStream) any())).thenReturn(new ReadyWithResult<SigningResult>() {
|
when(detachedSign.data((InputStream) any())).thenReturn(new ReadyWithResult<SigningResult>() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -125,13 +139,14 @@ public class SignCmdTest {
|
||||||
throw new IOException();
|
throw new IOException();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
|
assertGenericError(() ->
|
||||||
|
SopCLI.execute("sign", keyFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.ExpectedText.EXIT_CODE)
|
|
||||||
public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText {
|
public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText {
|
||||||
when(detachedSign.data((InputStream) any())).thenThrow(new SOPGPException.ExpectedText());
|
when(detachedSign.data((InputStream) any())).thenThrow(new SOPGPException.ExpectedText());
|
||||||
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()});
|
assertExpectedText(() ->
|
||||||
|
SopCLI.execute("sign", keyFile.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,11 @@ import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertNoSignature;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -21,7 +26,6 @@ import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
@ -76,60 +80,75 @@ public class VerifyCmdTest {
|
||||||
@Test
|
@Test
|
||||||
public void notAfter_passedDown() throws SOPGPException.UnsupportedOption, ParseException {
|
public void notAfter_passedDown() throws SOPGPException.UnsupportedOption, ParseException {
|
||||||
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z");
|
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z");
|
||||||
SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("verify", "--not-after", "2019-10-29T18:36:45Z",
|
||||||
|
signature.getAbsolutePath(), cert.getAbsolutePath()));
|
||||||
verify(detachedVerify, times(1)).notAfter(date);
|
verify(detachedVerify, times(1)).notAfter(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void notAfter_now() throws SOPGPException.UnsupportedOption {
|
public void notAfter_now() throws SOPGPException.UnsupportedOption {
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
SopCLI.main(new String[] {"verify", "--not-after", "now", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("verify", "--not-after", "now",
|
||||||
|
signature.getAbsolutePath(), cert.getAbsolutePath()));
|
||||||
verify(detachedVerify, times(1)).notAfter(dateMatcher(now));
|
verify(detachedVerify, times(1)).notAfter(dateMatcher(now));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void notAfter_dashCountsAsEndOfTime() throws SOPGPException.UnsupportedOption {
|
public void notAfter_dashCountsAsEndOfTime() throws SOPGPException.UnsupportedOption {
|
||||||
SopCLI.main(new String[] {"verify", "--not-after", "-", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("verify", "--not-after", "-",
|
||||||
|
signature.getAbsolutePath(), cert.getAbsolutePath()));
|
||||||
verify(detachedVerify, times(1)).notAfter(AbstractSopCmd.END_OF_TIME);
|
verify(detachedVerify, times(1)).notAfter(AbstractSopCmd.END_OF_TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
|
public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
|
||||||
when(detachedVerify.notAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported."));
|
when(detachedVerify.notAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported."));
|
||||||
SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
assertUnsupportedOption(() ->
|
||||||
|
SopCLI.execute("verify", "--not-after", "2019-10-29T18:36:45Z",
|
||||||
|
signature.getAbsolutePath(), cert.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void notBefore_passedDown() throws SOPGPException.UnsupportedOption, ParseException {
|
public void notBefore_passedDown() throws SOPGPException.UnsupportedOption, ParseException {
|
||||||
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z");
|
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z");
|
||||||
SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("verify", "--not-before", "2019-10-29T18:36:45Z",
|
||||||
|
signature.getAbsolutePath(), cert.getAbsolutePath()));
|
||||||
verify(detachedVerify, times(1)).notBefore(date);
|
verify(detachedVerify, times(1)).notBefore(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void notBefore_now() throws SOPGPException.UnsupportedOption {
|
public void notBefore_now() throws SOPGPException.UnsupportedOption {
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
SopCLI.main(new String[] {"verify", "--not-before", "now", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("verify", "--not-before", "now",
|
||||||
|
signature.getAbsolutePath(), cert.getAbsolutePath()));
|
||||||
verify(detachedVerify, times(1)).notBefore(dateMatcher(now));
|
verify(detachedVerify, times(1)).notBefore(dateMatcher(now));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void notBefore_dashCountsAsBeginningOfTime() throws SOPGPException.UnsupportedOption {
|
public void notBefore_dashCountsAsBeginningOfTime() throws SOPGPException.UnsupportedOption {
|
||||||
SopCLI.main(new String[] {"verify", "--not-before", "-", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("verify", "--not-before", "-",
|
||||||
|
signature.getAbsolutePath(), cert.getAbsolutePath()));
|
||||||
verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME);
|
verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
|
|
||||||
public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
|
public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
|
||||||
when(detachedVerify.notBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported."));
|
when(detachedVerify.notBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported."));
|
||||||
SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
assertUnsupportedOption(() ->
|
||||||
|
SopCLI.execute("verify", "--not-before", "2019-10-29T18:36:45Z",
|
||||||
|
signature.getAbsolutePath(), cert.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void notBeforeAndNotAfterAreCalledWithDefaultValues() throws SOPGPException.UnsupportedOption {
|
public void notBeforeAndNotAfterAreCalledWithDefaultValues() throws SOPGPException.UnsupportedOption {
|
||||||
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath()));
|
||||||
verify(detachedVerify, times(1)).notAfter(dateMatcher(new Date()));
|
verify(detachedVerify, times(1)).notAfter(dateMatcher(new Date()));
|
||||||
verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME);
|
verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME);
|
||||||
}
|
}
|
||||||
|
@ -139,43 +158,43 @@ public class VerifyCmdTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
|
|
||||||
public void cert_fileNotFoundCausesExit61() {
|
public void cert_fileNotFoundCausesExit61() {
|
||||||
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), "invalid.asc"});
|
assertMissingInput(() ->
|
||||||
|
SopCLI.execute("verify", signature.getAbsolutePath(), "invalid.asc"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
|
|
||||||
public void cert_badDataCausesExit41() throws SOPGPException.BadData, IOException {
|
public void cert_badDataCausesExit41() throws SOPGPException.BadData, IOException {
|
||||||
when(detachedVerify.cert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
when(detachedVerify.cert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
assertBadData(() ->
|
||||||
|
SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
|
|
||||||
public void signature_fileNotFoundCausesExit61() {
|
public void signature_fileNotFoundCausesExit61() {
|
||||||
SopCLI.main(new String[] {"verify", "invalid.sig", cert.getAbsolutePath()});
|
assertMissingInput(() ->
|
||||||
|
SopCLI.execute("verify", "invalid.sig", cert.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
|
|
||||||
public void signature_badDataCausesExit41() throws SOPGPException.BadData, IOException {
|
public void signature_badDataCausesExit41() throws SOPGPException.BadData, IOException {
|
||||||
when(detachedVerify.signatures((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
when(detachedVerify.signatures((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
assertBadData(() ->
|
||||||
|
SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.NoSignature.EXIT_CODE)
|
|
||||||
public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
|
public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
|
||||||
when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.NoSignature());
|
when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.NoSignature());
|
||||||
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
assertNoSignature(() ->
|
||||||
|
SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
|
|
||||||
public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
|
public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
|
||||||
when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
|
||||||
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
assertBadData(() ->
|
||||||
|
SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -192,7 +211,8 @@ public class VerifyCmdTest {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
System.setOut(new PrintStream(out));
|
System.setOut(new PrintStream(out));
|
||||||
|
|
||||||
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath()));
|
||||||
|
|
||||||
System.setOut(originalSout);
|
System.setOut(originalSout);
|
||||||
|
|
||||||
|
|
|
@ -4,19 +4,19 @@
|
||||||
|
|
||||||
package sop.cli.picocli.commands;
|
package sop.cli.picocli.commands;
|
||||||
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.times;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import sop.SOP;
|
import sop.SOP;
|
||||||
import sop.cli.picocli.SopCLI;
|
import sop.cli.picocli.SopCLI;
|
||||||
import sop.exception.SOPGPException;
|
|
||||||
import sop.operation.Version;
|
import sop.operation.Version;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
|
||||||
|
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption;
|
||||||
|
|
||||||
public class VersionCmdTest {
|
public class VersionCmdTest {
|
||||||
|
|
||||||
private Version version;
|
private Version version;
|
||||||
|
@ -29,6 +29,8 @@ public class VersionCmdTest {
|
||||||
when(version.getVersion()).thenReturn("1.0");
|
when(version.getVersion()).thenReturn("1.0");
|
||||||
when(version.getExtendedVersion()).thenReturn("MockSop Extended Version Information");
|
when(version.getExtendedVersion()).thenReturn("MockSop Extended Version Information");
|
||||||
when(version.getBackendVersion()).thenReturn("Foo");
|
when(version.getBackendVersion()).thenReturn("Foo");
|
||||||
|
when(version.getSopSpecVersion()).thenReturn("draft-dkg-openpgp-stateless-cli-XX");
|
||||||
|
when(version.getSopVVersion()).thenReturn("1.0");
|
||||||
when(sop.version()).thenReturn(version);
|
when(sop.version()).thenReturn(version);
|
||||||
|
|
||||||
SopCLI.setSopInstance(sop);
|
SopCLI.setSopInstance(sop);
|
||||||
|
@ -36,26 +38,41 @@ public class VersionCmdTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void assertVersionCommandWorks() {
|
public void assertVersionCommandWorks() {
|
||||||
SopCLI.main(new String[] {"version"});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("version"));
|
||||||
verify(version, times(1)).getVersion();
|
verify(version, times(1)).getVersion();
|
||||||
verify(version, times(1)).getName();
|
verify(version, times(1)).getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void assertExtendedVersionCommandWorks() {
|
public void assertExtendedVersionCommandWorks() {
|
||||||
SopCLI.main(new String[] {"version", "--extended"});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("version", "--extended"));
|
||||||
verify(version, times(1)).getExtendedVersion();
|
verify(version, times(1)).getExtendedVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void assertBackendVersionCommandWorks() {
|
public void assertBackendVersionCommandWorks() {
|
||||||
SopCLI.main(new String[] {"version", "--backend"});
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("version", "--backend"));
|
||||||
verify(version, times(1)).getBackendVersion();
|
verify(version, times(1)).getBackendVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
|
public void assertSpecVersionCommandWorks() {
|
||||||
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("version", "--sop-spec"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void assertSOPVVersionCommandWorks() {
|
||||||
|
assertSuccess(() ->
|
||||||
|
SopCLI.execute("version", "--sopv"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
public void assertInvalidOptionResultsInExit37() {
|
public void assertInvalidOptionResultsInExit37() {
|
||||||
SopCLI.main(new String[] {"version", "--invalid"});
|
assertUnsupportedOption(() ->
|
||||||
|
SopCLI.execute("version", "--invalid"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue