mirror of
https://codeberg.org/PGPainless/sop-java.git
synced 2025-09-09 18:29:48 +02:00
Compare commits
519 commits
Author | SHA1 | Date | |
---|---|---|---|
b28e234f21 | |||
cc08b76b68 | |||
e680f3450a | |||
d4e8c14b08 | |||
d32d9b54d7 | |||
c651adc0b3 | |||
b3223372c6 | |||
9762f1f043 | |||
191ec8c07d | |||
07d0aa6941 | |||
12835bfb8e | |||
04d154f63d | |||
01be696f75 | |||
86718a2690 | |||
e72e5a15c0 | |||
ac17000ff1 | |||
2a22cea29b | |||
8a7fd5cb58 | |||
21766a1f39 | |||
cdcbae7e5f | |||
ebfde35422 | |||
bff4423f93 | |||
5a7a8ae901 | |||
79aece6f04 | |||
28d06c330d | |||
ab13cc1de1 | |||
e1d048225b | |||
61206dde53 | |||
589884672a | |||
47a6db8702 | |||
0df80470c6 | |||
00a02686c8 | |||
c5d9e57f69 | |||
e481717421 | |||
9677f1fd0b | |||
e5cb58468b | |||
4ef5444e78 | |||
77106942d1 | |||
6d23d3771d | |||
9360b0e8ce | |||
38c5a947dd | |||
be460fabab | |||
138e275bb6 | |||
091b5f9a5e | |||
dea7e905a9 | |||
8c077a9c13 | |||
a8cfb8fbf4 | |||
a8497617d5 | |||
68bab9cbb4 | |||
65aa0afd4e | |||
a72545e3b9 | |||
082cbde869 | |||
b300be42a4 | |||
5105b6f4ad | |||
7f1c1b1aae | |||
4cf410a9f9 | |||
f1bdce99cb | |||
f7cc9ab816 | |||
40ccb8cc99 | |||
0bb50952c5 | |||
69fbfc09a7 | |||
dd12e28926 | |||
4ed326a142 | |||
122cd016a1 | |||
2b6a5dd651 | |||
d6c1330874 | |||
ada77be955 | |||
84404d629f | |||
4115a5041d | |||
1dcf13244d | |||
ad137d6351 | |||
cbeec9c90d | |||
701f9453ca | |||
2d99aea4ab | |||
4d2876a296 | |||
e3fe9410d7 | |||
a2a3bda2b3 | |||
cddc92bd92 | |||
8394f2e5a8 | |||
2c26ab2da5 | |||
859bb5bdde | |||
edb405d79e | |||
57e2f8391b | |||
51ba24ddbe | |||
d1893c5ea0 | |||
c145f8bb37 | |||
924cfaa140 | |||
f2602bb413 | |||
97e91f50ab | |||
690ba6dc16 | |||
9ec3cc911b | |||
f92a73a5ad | |||
2b6015f59a | |||
84e381fe8e | |||
b1e1a2283f | |||
b3b8da4e35 | |||
ca65cbe668 | |||
4eb6d1fdcb | |||
594b9029b2 | |||
471947ef9c | |||
1fd3161851 | |||
a8a753536a | |||
eadea08d3c | |||
547acdb740 | |||
bb026bcbeb | |||
e7778cb0d2 | |||
ac00b68694 | |||
e6c9d6f43d | |||
c136d40fa7 | |||
f35fd6c1ae | |||
375dd65789 | |||
42a16a4f6d | |||
b3f446fe8d | |||
1958614fac | |||
a09f10fe85 | |||
a90f9be0e4 | |||
63d8045224 | |||
7014dbcfb7 | |||
354ef8841a | |||
261ac212b8 | |||
f7530e3263 | |||
8d7e89098f | |||
a523270395 | |||
d25a424adc | |||
2d4bc24c64 | |||
65945e0094 | |||
4388f00dc0 | |||
1df5747549 | |||
ae2389cabf | |||
34a05e96a1 | |||
7b04275625 | |||
a0e7356757 | |||
173bc55eb9 | |||
03f8950b16 | |||
d5d7d67d6f | |||
e2a568e73e | |||
7092baee4f | |||
592aecd646 | |||
e5e64003f3 | |||
51d9c29837 | |||
ae83ddcff6 | |||
7eeb159f12 | |||
60758dfa2f | |||
6c952efca2 | |||
3eaae149b7 | |||
832a455c4c | |||
f2204dfd4d | |||
8dc51b67a3 | |||
7be71494cf | |||
f181453004 | |||
9b79a49bb5 | |||
01abae4d08 | |||
c53c69f3ac | |||
4a405f6d39 | |||
9cd9f151c9 | |||
03da9bbfb7 | |||
da2b299f4d | |||
d149aac56c | |||
6771952618 | |||
1c0666b4e1 | |||
d24ff9cbde | |||
802bc0aa73 | |||
03cabdf3fb | |||
3dde174880 | |||
2051c3632a | |||
0563105b1f | |||
72ca392386 | |||
a5c332737b | |||
41acdfe03a | |||
edef899074 | |||
baa44a6b1a | |||
0c2cf5cb19 | |||
5c2695228b | |||
b251956f49 | |||
b884f2b1a9 | |||
2e118357e2 | |||
e9a5467f6b | |||
019dd63e1b | |||
bfad8c4203 | |||
159ffbe084 | |||
714c933cef | |||
9daabb758a | |||
8e65771e36 | |||
688b8043a2 | |||
49120c5da8 | |||
377a7287b3 | |||
18865feaff | |||
666d51384b | |||
256d1c5960 | |||
8246359a85 | |||
1de179c015 | |||
86b173bf1c | |||
5ee9414410 | |||
a8829350a8 | |||
7824ee92c5 | |||
94b428ef62 | |||
e1a6ffd07a | |||
25a33611fd | |||
05886228df | |||
b7007cc007 | |||
01f98df80b | |||
30c369d24a | |||
be6be3deac | |||
1c290e0c8f | |||
d5c0d4e390 | |||
4b9e2c206f | |||
049c18c17b | |||
d0ee9c2066 | |||
a8c2e72ef5 | |||
0ee4638beb | |||
145cadef4f | |||
6c14f249bb | |||
be0ceb0886 | |||
9283f81c56 | |||
8df4a520bd | |||
3e6ebe1cc4 | |||
653675f730 | |||
41db9d2ac7 | |||
e681090757 | |||
ee6975c7d3 | |||
4dc1779a06 | |||
91a861b5c3 | |||
39c222dfc8 | |||
34e1d8992f | |||
4a123a1980 | |||
08ddc5d8a5 | |||
e68d6df57f | |||
31409b7949 | |||
dc23c8aa98 | |||
2391ffc9b2 | |||
a89e70c19e | |||
e6562cecff | |||
9dbb93e13d | |||
bbe159e88c | |||
0cb5c74a11 | |||
ef4b01c6bd | |||
6c5c4b3d98 | |||
567571cf6c | |||
0f5270c28d | |||
4bd4657906 | |||
cf1d39643d | |||
f2073dcbf4 | |||
308c4b452f | |||
be351616b6 | |||
530c44ad16 | |||
9ad59abb2a | |||
cd2c62ce2b | |||
edb384d157 | |||
feb9efc733 | |||
009364b217 | |||
f13aade98e | |||
d1c614344c | |||
6a579bff03 | |||
9ba005f7cc | |||
6afe6896d8 | |||
7e1377a28c | |||
618d123a7b | |||
e6393b44b9 | |||
ab2e4aa8e7 | |||
f65ddba4b4 | |||
bfaba69222 | |||
7ab65f63a4 | |||
b8ad6d77a2 | |||
ab8f44138d | |||
419056ba4c | |||
312cdb69c9 | |||
c479cc8ef3 | |||
aa88904711 | |||
7ea46a1916 | |||
49fd7143cf | |||
8aded17f10 | |||
8eba099146 | |||
0308732328 | |||
8b8863c6df | |||
44e6dd2180 | |||
19d6b7e142 | |||
226b5d99a0 | |||
e336e536a8 | |||
ed59c713eb | |||
0aabfac695 | |||
790d80ec29 | |||
0fccf3051c | |||
a722e98578 | |||
aeda534f37 | |||
bb2b4e03fb | |||
4a7c2b74da | |||
78ecf2f554 | |||
d8cac7b9d7 | |||
51a7d950e5 | |||
41260bb02c | |||
415fc3dd3a | |||
84a01df4bd | |||
1a4affde35 | |||
8425665fa7 | |||
90c77706a8 | |||
8a66f0bc4f | |||
f4ff5f89f7 | |||
f49c16e4c5 | |||
dfce1ad6bb | |||
925505989e | |||
42bd8f06a4 | |||
3f4ec072a9 | |||
146f24eab8 | |||
a3bff6f6d1 | |||
8a9f535531 | |||
7e12da400b | |||
360f2fba02 | |||
d838e9589b | |||
ffdd5eee51 | |||
67292864b3 | |||
5d2f87eb80 | |||
6ec62560ba | |||
7743f15e72 | |||
9bc391fc7c | |||
64c0fb11bc | |||
d38556f79a | |||
19663c4dec | |||
1e805db1f0 | |||
dff5b01eec | |||
5935d65c90 | |||
b8544396f8 | |||
0ed5c52f4b | |||
83a003e80f | |||
17b305924c | |||
6d28a7b07d | |||
f5e34bce6c | |||
5d04bb965b | |||
ae64414492 | |||
f37354d268 | |||
6448debf46 | |||
d488eee36f | |||
9fdc8a5bad | |||
546b97fcc9 | |||
40dc9e3707 | |||
6ac133499c | |||
6fad442cd0 | |||
0709bce35c | |||
fd426b533c | |||
88e3ba0095 | |||
6c3e148bcd | |||
c1ae5314a0 | |||
3789b60f0b | |||
0b96a5314f | |||
0c8f6baf98 | |||
8cacf7dd57 | |||
e73c7e5f91 | |||
9cf6301b8c | |||
d09626782d | |||
c95ca8fedc | |||
0d9db2bdd3 | |||
ffc5b26c0d | |||
61c5eb2962 | |||
104b3a4ec4 | |||
0616cde6fd | |||
990d314709 | |||
4fc8ffab42 | |||
78b5eea630 | |||
b42d0e89a1 | |||
8fc88b5bab | |||
eded55c259 | |||
4726362df8 | |||
670aa0f242 | |||
125eefed6e | |||
6e40c7dc17 | |||
bd02b11944 | |||
951cf9cbca | |||
b15acc79b3 | |||
f1c6fd67d3 | |||
85f61b413f | |||
9c27141c00 | |||
909e28432d | |||
d079a345d2 | |||
e3b618a0a8 | |||
3eb2503852 | |||
a63b29fe80 | |||
efec4d9110 | |||
e602cc16cc | |||
28912618ea | |||
ed296ec4b2 | |||
3bc19b27ad | |||
eddcc11c99 | |||
ff5f98e8ee | |||
14c665565e | |||
d9708e882d | |||
2c3717157a | |||
2328bdf6af | |||
ae128d2cbb | |||
55f196b241 | |||
62d3ffd9ae | |||
519fb891a1 | |||
0e777de14f | |||
0ed7163fd5 | |||
ed46adbe52 | |||
60f52ab89e | |||
3a6905c0bd | |||
00ab68b504 | |||
1a381becfa | |||
4bc45a0692 | |||
3ee42ea2ed | |||
114ee94f0d | |||
82456ec9e1 | |||
c00109c2bf | |||
01beb99e43 | |||
6f20f78339 | |||
76cc9098ce | |||
7326d3cf85 | |||
137d2e7f85 | |||
dad75bb522 | |||
9ea9cd22b8 | |||
5cde383b1a | |||
f60e14de96 | |||
8494f413e6 | |||
aa953428ee | |||
dd5d790e21 | |||
40919be3f7 | |||
4ba864e1cd | |||
46e62e9e40 | |||
60ef0a15a8 | |||
1aaac4e4b9 | |||
fd4c22cde6 | |||
c32ef9830b | |||
e75dde1637 | |||
162ae120c3 | |||
043e95e5c0 | |||
04d38b988a | |||
29660a1d3f | |||
a14f008215 | |||
06d92eecf3 | |||
e17d4d2efb | |||
092a83a1e7 | |||
0cb614827b | |||
dcb44f96c8 | |||
dc5f11469f | |||
d80a0a067f | |||
c4cbf8ff69 | |||
fe729c4eb8 | |||
b7c1b4f1a1 | |||
01dbaab598 | |||
a651abb44e | |||
ed150e51a8 | |||
1e9b1c77d3 | |||
5fb1146dfb | |||
8c581d459a | |||
7de94f1815 | |||
fa52df385e | |||
3801a644ef | |||
86e39809ae | |||
77c76c57d0 | |||
084923c828 | |||
bfce786433 | |||
1ca1bb5671 | |||
55673b52a7 | |||
8db710a582 | |||
61ab35ad52 | |||
0c24c2301d | |||
eefb445916 | |||
e175ee6208 | |||
6d5005660c | |||
2135455cda | |||
4934e472e2 | |||
a7f02d58cc | |||
8184c30617 | |||
45ee435a18 | |||
6438ebc59c | |||
de11c17967 | |||
55aefb799f | |||
8406793d0e | |||
9016c9f428 | |||
580c3af350 | |||
e8d7d1b5f4 | |||
72c3b3218d | |||
fd9192995f | |||
0bfc12c1e1 | |||
0699ae740a | |||
146a2bd761 | |||
be2fd9de49 | |||
33c8e5edc5 | |||
4e74fc6aa3 | |||
417115dfe1 | |||
3dbeda45f2 | |||
5c1862fe7d | |||
a6be316ed2 | |||
aa346459e2 | |||
8753480e82 | |||
0065401c15 | |||
807689d516 | |||
ea9116cec5 | |||
5178167214 | |||
eb80c14ce3 | |||
69b9881798 | |||
ff0506d29c | |||
bdd18c1a11 | |||
e154b3e535 | |||
cec2ad8c1e | |||
1030de2ad3 | |||
06cea1235a | |||
4b6f00e40e | |||
866bc92d07 | |||
6c392addc1 | |||
cb16c1ce84 | |||
bbd5b75427 | |||
582570166c | |||
e6d6f0337f | |||
4177780653 | |||
780abbbc51 | |||
e88d9f9dab | |||
3b316bd06b | |||
25915d1204 | |||
8877bae675 | |||
3e1502ff2a | |||
117117b735 | |||
a2f2069380 | |||
c2637c38eb | |||
7bb06939e7 | |||
9950668792 | |||
7db949d79b | |||
b2cca38ba7 | |||
1b24da7f1e | |||
f4e882ce8b |
269 changed files with 14104 additions and 3799 deletions
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. -->
|
21
.woodpecker/build.yml
Normal file
21
.woodpecker/build.yml
Normal file
|
@ -0,0 +1,21 @@
|
|||
steps:
|
||||
run:
|
||||
when:
|
||||
event: push
|
||||
image: gradle:7.6-jdk11-jammy
|
||||
commands:
|
||||
# Install Sequoia-SOP
|
||||
- apt update && apt install --yes sqop
|
||||
# Checkout code
|
||||
- git checkout $CI_COMMIT_BRANCH
|
||||
# Prepare CI
|
||||
- cp external-sop/src/main/resources/sop/testsuite/external/config.json.ci external-sop/src/main/resources/sop/testsuite/external/config.json
|
||||
# Code works
|
||||
- gradle test
|
||||
# Code is clean
|
||||
- gradle check javadocAll
|
||||
# Code has coverage
|
||||
- gradle jacocoRootReport coveralls
|
||||
environment:
|
||||
COVERALLS_REPO_TOKEN:
|
||||
from_secret: coveralls_repo_token
|
9
.woodpecker/reuse.yml
Normal file
9
.woodpecker/reuse.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Code is licensed properly
|
||||
# See https://reuse.software/
|
||||
steps:
|
||||
reuse:
|
||||
when:
|
||||
event: push
|
||||
image: fsfe/reuse:latest
|
||||
commands:
|
||||
- reuse lint
|
222
CHANGELOG.md
222
CHANGELOG.md
|
@ -1,5 +1,225 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
# Changelog
|
||||
|
||||
## 14.0.0
|
||||
- Update implementation to [SOP Specification revision 14](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-14.html),
|
||||
including changes from revisions `11`, `12`, `13`, `14`.
|
||||
- Implement newly introduced operations
|
||||
- `update-key` 'fixes' everything wrong with a key
|
||||
- `merge-certs` merges a certificate with other copies
|
||||
- `certify-userid` create signatures over user-ids on certificates
|
||||
- `validate-userid` validate signatures over user-ids
|
||||
- Add new exceptions
|
||||
- `UnspecificFailure` maps generic application errors
|
||||
- `KeyCannotCertify` signals that a key cannot be used for third-party certifications
|
||||
- `NoHardwareKeyFound` signals that a key backed by a hardware device cannot be found
|
||||
- `HardwareKeyFailure` signals a hardware device failure
|
||||
- `PrimaryKeyBad` signals an unusable or bad primary key
|
||||
- `CertUserIdNoMatch` signals that a user-id cannot be found/validated on a certificate
|
||||
- `Verification`: Add support for JSON description extensions
|
||||
- Remove `animalsniffer` from build dependencies
|
||||
- Bump `logback` to `1.5.13`
|
||||
|
||||
## 10.1.1
|
||||
- Prepare jar files for use in native images, e.g. using GraalVM by generating and including
|
||||
configuration files for reflection, resources and dynamic proxies.
|
||||
- gradle: Make use of jvmToolchain functionality
|
||||
- gradle: Improve reproducibility
|
||||
- gradle: Bump animalsniffer to `2.0.0`
|
||||
|
||||
## 10.1.0
|
||||
- `sop-java`:
|
||||
- Remove `label()` option from `armor()` subcommand
|
||||
- Move test-fixtures artifact built with the `testFixtures` plugin into
|
||||
its own module `sop-java-testfixtures`, which can be consumed by maven builds.
|
||||
- `sop-java-picocli`:
|
||||
- Properly map `MissingParameterException` to `MissingArg` exit code
|
||||
- As a workaround for native builds using graalvm:
|
||||
- Do not re-set message bundles dynamically (fails in native builds)
|
||||
- Prevent an unmatched argument error
|
||||
|
||||
## 10.0.3
|
||||
- CLI `change-key-password`: Fix indirect parameter passing for new and old passwords (thanks to @dkg for the report)
|
||||
- Backport: `revoke-key`: Allow for multiple password options
|
||||
|
||||
## 10.0.2
|
||||
- Downgrade `logback-core` to `1.2.13`
|
||||
|
||||
## 10.0.1
|
||||
- Remove `label()` option from `Armor` operation
|
||||
- Fix exit code for 'Missing required option/parameter' error
|
||||
- Fix `revoke-key`: Allow for multiple invocations of `--with-key-password` option
|
||||
- Fix `EncryptExternal` use of `--sign-with` parameter
|
||||
- Fix `NullPointerException` in `DecryptExternal` when reading lines
|
||||
- Fix `DecryptExternal` use of `verifications-out`
|
||||
- Test suite: Ignore tests if `UnsupportedOption` is thrown
|
||||
- Bump `logback-core` to `1.4.14`
|
||||
|
||||
## 10.0.0
|
||||
- Update implementation to [SOP Specification revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html).
|
||||
- Throw `BadData` when passing KEYS where CERTS are expected
|
||||
- Introduce `sopv` interface subset with revision `1.0`
|
||||
- Add `sop version --sopv`
|
||||
|
||||
## 8.0.2
|
||||
- CLI `change-key-password`: Fix indirect parameter passing for new and old passwords (thanks to @dkg for the report)
|
||||
- Backport: `revoke-key`: Allow for multiple password options
|
||||
|
||||
## 8.0.1
|
||||
- `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty.
|
||||
|
||||
## 8.0.0
|
||||
- Rewrote `sop-java` in Kotlin
|
||||
- Rewrote `sop-java-picocli` in Kotlin
|
||||
- Rewrote `external-sop` in Kotlin
|
||||
- Update implementation to [SOP Specification revision 08](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html).
|
||||
- Add `--no-armor` option to `revoke-key` and `change-key-password` subcommands
|
||||
- `armor`: Deprecate `--label` option in `sop-java` and remove in `sop-java-picocli`
|
||||
- `encrypt`: Add `--session-key-out` option
|
||||
- Slight API changes:
|
||||
- `sop.encrypt().plaintext()` now returns a `ReadyWithResult<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.2
|
||||
- CLI `change-key-password`: Fix indirect parameter passing for new and old passwords (thanks to @dkg for the report)
|
||||
- Backport: revoke-key command: Allow for multiple '--with-key-password' options
|
||||
|
||||
## 7.0.1
|
||||
- `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty.
|
||||
|
||||
## 7.0.0
|
||||
- Update implementation to [SOP Specification revision 07](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-07.html).
|
||||
- Add support for new `revoke-key` subcommand
|
||||
- Add support for new `change-key-password` subcommand
|
||||
- Add support for new `--signing-only` option of `generate-key` subcommand
|
||||
- Add `dearmor.data(String)` utility method
|
||||
- Fix typos in, and improve i18n of CLI help pages
|
||||
|
||||
## 6.1.0
|
||||
- `listProfiles()`: Add shortcut methods `generateKey()` and `encrypt()`
|
||||
- Add DSL for testing `Verification` results
|
||||
- `Verification`
|
||||
- Return `Optional<SignatureMode>` for `getSignatureMode()`
|
||||
- Return `Optional<String>` for `getDescription()`
|
||||
- `Profile`
|
||||
- Add support for profiles without description
|
||||
- Return `Optional<String>` for `getDescription()`
|
||||
- Add `parse(String)` method for parsing profile lines
|
||||
- `sop-java`: Add dependency on `com.google.code.findbugs:jsr305` for `@Nullable`, `@Nonnull` annotations
|
||||
- `UTCUtil`: `parseUTCDate()` is now `@Nonnull` and throws a `ParseException` for invalid inputs
|
||||
- `UTF8Util`: `decodeUTF8()` now throws `CharacterCodingException` instead of `SOPGPException.PasswordNotHumanReadable`
|
||||
- `external-sop`: Properly map error codes to new exception types (ported from `5.0.1`):
|
||||
- `UNSUPPORTED_PROFILE`
|
||||
- `INCOMPATIBLE_OPTIONS`
|
||||
|
||||
## 5.0.1
|
||||
- `external-sop`: Properly map error codes to new exception types:
|
||||
- `UNSUPPORTED_PROFILE`
|
||||
- `INCOMPATIBLE_OPTIONS`
|
||||
|
||||
## 6.0.0
|
||||
- Update implementation to [SOP Specification revision 06](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-06.html).
|
||||
- Add option `--profile=XYZ` to `encrypt` subcommand
|
||||
- Add option `--sop-spec` to `version` subcommand
|
||||
- `Version`: Add different getters for specification-related values
|
||||
|
||||
## 5.0.0
|
||||
- Update implementation to [SOP Specification revision 05](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html).
|
||||
- Add the concept of profiles
|
||||
- Add `list-profiles` subcommand
|
||||
- Add option `--profile=XYZ` to `generate-key` subcommand
|
||||
- `Verification` objects can now optionally indicate the type of the signature (`mode:text` or `mode:binary`)
|
||||
- `Verification` objects can now contain an optional description of the signature
|
||||
- `inline-sign` now throws an error if incompatible options `--as=clearsigned` and `--no-armor` are used
|
||||
|
||||
## 4.1.1
|
||||
- Restructure test suite to allow simultaneous testing of multiple backends
|
||||
- Fix IOException in `sop sign` due to premature stream closing
|
||||
- Allow for downstream implementations of `sop-java` to reuse the test suite
|
||||
- Check out Javadoc of `sop-java/src/testFixtures/java/sop/testsuite/SOPInstanceFactory` for details
|
||||
|
||||
## 4.1.0
|
||||
- Add module `external-sop`
|
||||
- This module implements the `sop-java` interfaces and allows the use of an external SOP binary
|
||||
- `decrypt`: Rename `--not-before`, `--not-after` to `--verify-not-before`, `--verify-not-after`
|
||||
- `decrypt`: Throw `NoSignature` error if no verifiable signature found, but signature verification is requested using `--verify-with`.
|
||||
- `inline-sign`: Fix parameter label of `--as=clearsigned`
|
||||
- `ArmorLabel`, `EncryptAs`, `SignAs`: make `toString()` return lowercase
|
||||
|
||||
## 4.0.7
|
||||
- Make i18n string for `--stacktrace` option translatable
|
||||
- Make manpages generation reproducible
|
||||
- `dearmor`: Transform `IOException` into `BadData`
|
||||
|
||||
## 4.0.6
|
||||
- Add support for file descriptors on unix / linux systems
|
||||
|
||||
## 4.0.5
|
||||
- `inline-sign`: Make possible values of `--as` option lowercase
|
||||
- `inline-sign`: Rename value `cleartextsigned` of option `--as` to `clearsigned`
|
||||
|
||||
## 4.0.4
|
||||
- Not found
|
||||
|
||||
## 4.0.3
|
||||
- `decrypt`: Rename option `--verify-out` to `--verifications-out`, but keep `--verify-out` as alias
|
||||
- Fix: `decrypt`: Flush output stream in order to prevent empty file as result of `--session-key-out`
|
||||
- Fix: Properly format session key for `--session-key-out`
|
||||
- Be less finicky about input session key formats
|
||||
- Allow upper- and lowercase hexadecimal keys
|
||||
- Allow trailing whitespace
|
||||
|
||||
## 4.0.2
|
||||
- Fix: `verify`: Do not include detached signature in list of certificates
|
||||
- Fix: `inline-verify`: Also include the first argument in list of certificates
|
||||
- Hide stacktraces by default and add `--stacktrace` option to print them
|
||||
- Properly throw `CannotDecrypt` exception when message could not be decrypted
|
||||
|
||||
## 4.0.1
|
||||
- Use shared resources for i18n
|
||||
- Fix strings not being resolved properly when downstream renames `sop` command
|
||||
|
||||
## 4.0.0
|
||||
- Switch to new versioning format to indicate implemented SOP version
|
||||
- Implement SOP specification version 04
|
||||
- Add `--with-key-password` to `sop generate-key`
|
||||
- Add `--with-key-password` to `sop sign`
|
||||
- Add `--with-key-password` to `sop encrypt`
|
||||
- Add `--with-key-password` to `sop decrypt`
|
||||
- Rename `sop detach-inband-signature-and-message` to `sop inline-detach`
|
||||
- `sop inline-detach`: Add support for inline-signed messages
|
||||
- Implement `sop inline-sign`
|
||||
- Implement `sop inline-verify`
|
||||
- Rename `Sign` to `DetachedSign`
|
||||
- Rename `Verify` to `DetachedVerify`
|
||||
- `SignAs`: Remove `Mime` option
|
||||
- `sop-java-picocli`: Implement i18n and add German translation
|
||||
|
||||
## 1.2.3
|
||||
- Bump Mockito version to `4.5.1`
|
||||
|
||||
## 1.2.2
|
||||
- Add SOP parent command name and description
|
||||
|
||||
## 1.2.1
|
||||
- Bump dependencies
|
||||
- `com.ginsberg:junit5-system-exit` from `1.1.1` to `1.1.2`
|
||||
- `org.mockito:mockito-core` from `4.2.0` to `4.3.1`
|
||||
- `info.picocli:picocli` from `4.6.2` to `4.6.3`
|
||||
- Add hidden `generate-completion` subcommand
|
||||
- Document exit codes
|
||||
|
||||
## 1.2.0
|
||||
- `encrypt`, `decrypt`: Interpret arguments of `--with-password` and `--with-session-key` as indirect data types (e.g. file references instead of strings)
|
||||
|
||||
## 1.1.0
|
||||
- Initial release from new repository
|
||||
- Implement SOP specification version 3
|
||||
- Implement SOP specification version 3
|
||||
|
|
192
LICENSE
Normal file
192
LICENSE
Normal file
|
@ -0,0 +1,192 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
============================================================================
|
||||
|
||||
# Licenses for included dependencies
|
||||
|
||||
## [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
* info.picocli:picocli
|
||||
|
||||
## [Eclipe Public License 2.0](https://www.eclipse.org/legal/epl-2.0/)
|
||||
* org.junit.jupiter:junit-jupiter-api
|
||||
* org.junit.jupiter:junit-jupiter-params
|
||||
* org.junit.jupiter:junit-jupiter-engine
|
||||
|
||||
## [MIT License](https://opensource.org/licenses/MIT)
|
||||
* com.ginsberg:junit5-system-exit
|
||||
* org.mockito:mockito-core
|
73
LICENSES/Apache-2.0.txt
Normal file
73
LICENSES/Apache-2.0.txt
Normal file
|
@ -0,0 +1,73 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
121
LICENSES/CC0-1.0.txt
Normal file
121
LICENSES/CC0-1.0.txt
Normal file
|
@ -0,0 +1,121 @@
|
|||
Creative Commons Legal Code
|
||||
|
||||
CC0 1.0 Universal
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the
|
||||
Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
44
README.md
44
README.md
|
@ -1,23 +1,47 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
# SOP for Java
|
||||
|
||||
The [Stateless OpenPGP Protocol](https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-03) specification
|
||||
[](https://ci.codeberg.org/PGPainless/sop-java)
|
||||
[](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/14/)
|
||||
[](https://coveralls.io/github/pgpainless/sop-java?branch=main)
|
||||
[](https://api.reuse.software/info/github.com/pgpainless/sop-java)
|
||||
|
||||
The [Stateless OpenPGP Protocol](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) specification
|
||||
defines a generic stateless CLI for dealing with OpenPGP messages.
|
||||
Its goal is to provide a minimal, yet powerful API for the most common OpenPGP related operations.
|
||||
|
||||
`sop-java` defines a set of Java interfaces describing said API.
|
||||
[](https://repology.org/project/pgpainless/versions)
|
||||
[](https://search.maven.org/artifact/org.pgpainless/sop-java)
|
||||
|
||||
`sop-java-picocli` contains a wrapper application that transforms the `sop-java` API into a command line application
|
||||
## Modules
|
||||
|
||||
The repository contains the following modules:
|
||||
|
||||
* [sop-java](/sop-java) defines a set of Java interfaces describing the Stateless OpenPGP Protocol.
|
||||
* [sop-java-picocli](/sop-java-picocli) contains a wrapper application that transforms the `sop-java` API into a command line application
|
||||
compatible with the SOP-CLI specification.
|
||||
* [external-sop](/external-sop) contains an API implementation that can be used to forward API calls to a SOP executable,
|
||||
allowing to delegate the implementation logic to an arbitrary SOP CLI implementation.
|
||||
* [sop-java-testfixtures](/sop-java-testfixtures) contains a test suite that can be shared by downstream implementations
|
||||
of `sop-java`.
|
||||
|
||||
## Known Implementations
|
||||
(Please expand!)
|
||||
|
||||
| Project | Description |
|
||||
|---------------------------------------------------------------------------------------|-----------------------------------------------|
|
||||
| [pgpainless-sop](https://github.com/pgpainless/pgpainless/tree/master/pgpainless-sop) | Implementation of `sop-java` using PGPainless |
|
||||
| Project | Description |
|
||||
|-------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
|
||||
| [pgpainless-sop](https://github.com/pgpainless/pgpainless/tree/main/pgpainless-sop) | Implementation of `sop-java` using PGPainless |
|
||||
| [external-sop](https://github.com/pgpainless/sop-java/tree/main/external-sop) | Implementation of `sop-java` that allows binding to external SOP binaries such as `sqop` |
|
||||
| [bcsop](https://codeberg.org/PGPainless/bc-sop) | Implementation of `sop-java` using vanilla Bouncy Castle |
|
||||
|
||||
### Implementations in other languages
|
||||
| Project | Language |
|
||||
|-------------------------------------------------|----------|
|
||||
| [sop-rs](https://sequoia-pgp.gitlab.io/sop-rs/) | Rust |
|
||||
| [SOP for python](https://pypi.org/project/sop/) | Python |
|
||||
| Project | Language |
|
||||
|---------------------------------------------------|----------|
|
||||
| [sop-rs](https://sequoia-pgp.gitlab.io/sop-rs/) | Rust |
|
||||
| [SOP for python](https://pypi.org/project/sop/) | Python |
|
||||
| [rpgpie-sop](https://crates.io/crates/rpgpie-sop) | Rust |
|
||||
|
|
32
REUSE.toml
Normal file
32
REUSE.toml
Normal file
|
@ -0,0 +1,32 @@
|
|||
# SPDX-FileCopyrightText: 2025 Paul Schaub <info@pgpainless.org>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
version = 1
|
||||
SPDX-PackageName = "SOP-Java"
|
||||
SPDX-PackageSupplier = "Paul Schaub <info@pgpainless.org>"
|
||||
SPDX-PackageDownloadLocation = "https://pgpainless.org"
|
||||
|
||||
[[annotations]]
|
||||
path = "gradle**"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2015 the original author or authors."
|
||||
SPDX-License-Identifier = "Apache-2.0"
|
||||
|
||||
[[annotations]]
|
||||
path = ".woodpecker/**"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2022 the original author or authors."
|
||||
SPDX-License-Identifier = "Apache-2.0"
|
||||
|
||||
[[annotations]]
|
||||
path = "external-sop/src/main/resources/sop/testsuite/external/**"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2023 the original author or authors"
|
||||
SPDX-License-Identifier = "Apache-2.0"
|
||||
|
||||
[[annotations]]
|
||||
path = ".github/ISSUE_TEMPLATE/**"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2024 the original author or authors"
|
||||
SPDX-License-Identifier = "Apache-2.0"
|
60
build.gradle
60
build.gradle
|
@ -18,7 +18,8 @@ buildscript {
|
|||
}
|
||||
|
||||
plugins {
|
||||
id 'ru.vyarus.animalsniffer' version '1.5.3'
|
||||
id 'org.jetbrains.kotlin.jvm' version "1.9.21"
|
||||
id 'com.diffplug.spotless' version '6.22.0' apply false
|
||||
}
|
||||
|
||||
apply from: 'version.gradle'
|
||||
|
@ -29,18 +30,9 @@ allprojects {
|
|||
apply plugin: 'eclipse'
|
||||
apply plugin: 'jacoco'
|
||||
apply plugin: 'checkstyle'
|
||||
|
||||
// For non-cli modules enable android api compatibility check
|
||||
if (it.name.equals('sop-java')) {
|
||||
// animalsniffer
|
||||
apply plugin: 'ru.vyarus.animalsniffer'
|
||||
dependencies {
|
||||
signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:2.3.3_r2@signature"
|
||||
}
|
||||
animalsniffer {
|
||||
sourceSets = [sourceSets.main]
|
||||
}
|
||||
}
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.diffplug.spotless'
|
||||
|
||||
// Only generate jar for submodules
|
||||
// https://stackoverflow.com/a/25445035
|
||||
|
@ -53,12 +45,16 @@ allprojects {
|
|||
toolVersion = '8.18'
|
||||
}
|
||||
|
||||
spotless {
|
||||
kotlin {
|
||||
ktfmt().dropboxStyle()
|
||||
}
|
||||
}
|
||||
|
||||
group 'org.pgpainless'
|
||||
description = "Stateless OpenPGP Protocol API for Java"
|
||||
version = shortVersion
|
||||
|
||||
sourceCompatibility = javaSourceCompatibility
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
@ -67,13 +63,23 @@ allprojects {
|
|||
tasks.withType(AbstractArchiveTask) {
|
||||
preserveFileTimestamps = false
|
||||
reproducibleFileOrder = true
|
||||
|
||||
dirMode = 0755
|
||||
fileMode = 0644
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(javaSourceCompatibility)
|
||||
}
|
||||
|
||||
// Compatibility of default implementations in kotlin interfaces with Java implementations.
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += ["-Xjvm-default=all-compatibility"]
|
||||
}
|
||||
}
|
||||
|
||||
project.ext {
|
||||
slf4jVersion = '1.7.32'
|
||||
logbackVersion = '1.2.9'
|
||||
junitVersion = '5.8.2'
|
||||
picocliVersion = '4.6.2'
|
||||
rootConfigDir = new File(rootDir, 'config')
|
||||
gitCommit = getGitCommit()
|
||||
isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI'))
|
||||
|
@ -98,7 +104,7 @@ allprojects {
|
|||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion = "0.8.7"
|
||||
toolVersion = "0.8.8"
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
|
@ -106,7 +112,7 @@ allprojects {
|
|||
sourceDirectories.setFrom(project.files(sourceSets.main.allSource.srcDirs))
|
||||
classDirectories.setFrom(project.files(sourceSets.main.output))
|
||||
reports {
|
||||
xml.enabled true
|
||||
xml.required = true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,15 +130,15 @@ subprojects {
|
|||
apply plugin: 'signing'
|
||||
|
||||
task sourcesJar(type: Jar, dependsOn: classes) {
|
||||
classifier = 'sources'
|
||||
archiveClassifier = 'sources'
|
||||
from sourceSets.main.allSource
|
||||
}
|
||||
task javadocJar(type: Jar, dependsOn: javadoc) {
|
||||
classifier = 'javadoc'
|
||||
archiveClassifier = 'javadoc'
|
||||
from javadoc.destinationDir
|
||||
}
|
||||
task testsJar(type: Jar, dependsOn: testClasses) {
|
||||
classifier = 'tests'
|
||||
archiveClassifier = 'tests'
|
||||
from sourceSets.test.output
|
||||
}
|
||||
|
||||
|
@ -229,7 +235,7 @@ task jacocoRootReport(type: JacocoReport) {
|
|||
classDirectories.setFrom(files(subprojects.sourceSets.main.output))
|
||||
executionData.setFrom(files(subprojects.jacocoTestReport.executionData))
|
||||
reports {
|
||||
xml.enabled true
|
||||
xml.required = true
|
||||
xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml")
|
||||
}
|
||||
// We could remove the following setOnlyIf line, but then
|
||||
|
@ -240,10 +246,6 @@ task jacocoRootReport(type: JacocoReport) {
|
|||
}
|
||||
|
||||
task javadocAll(type: Javadoc) {
|
||||
def currentJavaVersion = JavaVersion.current()
|
||||
if (currentJavaVersion.compareTo(JavaVersion.VERSION_1_9) >= 0) {
|
||||
options.addStringOption("-release", "8");
|
||||
}
|
||||
source subprojects.collect {project ->
|
||||
project.sourceSets.main.allJava }
|
||||
destinationDir = new File(buildDir, 'javadoc')
|
||||
|
|
59
external-sop/README.md
Normal file
59
external-sop/README.md
Normal file
|
@ -0,0 +1,59 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
# External-SOP
|
||||
|
||||
Access an external SOP binary from within your Java/Kotlin application.
|
||||
|
||||
This module implements a backend for `sop-java` that binds to external SOP binaries (such as
|
||||
[sqop](https://gitlab.com/sequoia-pgp/sequoia-sop/), [python-sop](https://pypi.org/project/sop/) etc.).
|
||||
SOP operation calls will be delegated to the external binary, and the results are parsed back, so that you can
|
||||
access them from your Java application as usual.
|
||||
|
||||
## Example
|
||||
Let's say you are using `ExampleSOP` which is a binary installed in `/usr/bin/example-sop`.
|
||||
Instantiating a `SOP` object is as simple as this:
|
||||
|
||||
```java
|
||||
SOP sop = new ExternalSOP("/usr/bin/example-sop");
|
||||
```
|
||||
|
||||
This SOP object can now be used as usual (see [here](../sop-java/README.md)).
|
||||
|
||||
Keep in mind the license of the external SOP binary when integrating one with your project!
|
||||
|
||||
Some SOP binaries might require additional configuration, e.g. a Java based SOP might need to know which JAVA_HOME to use.
|
||||
For this purpose, additional environment variables can be passed in using a `Properties` object:
|
||||
|
||||
```java
|
||||
Properties properties = new Properties();
|
||||
properties.put("JAVA_HOME", "/usr/lib/jvm/[...]");
|
||||
SOP sop = new ExternalSOP("/usr/bin/example-sop", properties);
|
||||
```
|
||||
|
||||
Most results of SOP operations are communicated via standard-out, standard-in. However, some operations rely on
|
||||
writing results to additional output files.
|
||||
To handle such results, we need to provide a temporary directory, to which those results can be written by the SOP,
|
||||
and from which `External-SOP` reads them back.
|
||||
The default implementation relies on `Files.createTempDirectory()` to provide a temporary directory.
|
||||
It is however possible to overwrite this behavior, in order to specify a custom, perhaps more private directory:
|
||||
|
||||
```java
|
||||
ExternalSOP.TempDirProvider provider = new ExternalSOP.TempDirProvider() {
|
||||
@Override
|
||||
public File provideTempDirectory() throws IOException {
|
||||
File myTempDir = new File("/path/to/directory");
|
||||
myTempDir.mkdirs();
|
||||
return myTempDir;
|
||||
}
|
||||
};
|
||||
SOP sop = new ExternalSOP("/usr/bin/example-sop", provider);
|
||||
```
|
||||
|
||||
## Testing
|
||||
The `external-sop` module comes with a growing test suite, which tests SOP binaries against the expectations of the SOP specification.
|
||||
To configure one or multiple backends for use with the test suite, just provide a custom `config.json` file in `src/main/resources/sop/external`.
|
||||
An example configuration file with the required file format is available as `config.json.example`.
|
45
external-sop/build.gradle
Normal file
45
external-sop/build.gradle
Normal file
|
@ -0,0 +1,45 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
plugins {
|
||||
id 'java-library'
|
||||
}
|
||||
|
||||
group 'org.pgpainless'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
|
||||
|
||||
testImplementation "org.junit.platform:junit-platform-suite-api:1.13.2"
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-suite:1.13.2'
|
||||
|
||||
api project(":sop-java")
|
||||
api "org.slf4j:slf4j-api:$slf4jVersion"
|
||||
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
|
||||
|
||||
// @Nonnull, @Nullable...
|
||||
implementation "com.google.code.findbugs:jsr305:$jsrVersion"
|
||||
|
||||
// The ExternalTestSubjectFactory reads json config file to find configured SOP binaries...
|
||||
testImplementation "com.google.code.gson:gson:$gsonVersion"
|
||||
// ...and extends TestSubjectFactory
|
||||
testImplementation(project(":sop-java-testfixtures"))
|
||||
}
|
||||
|
||||
test {
|
||||
// Inject configured external SOP instances using our custom TestSubjectFactory
|
||||
environment("test.implementation", "sop.testsuite.external.ExternalSOPInstanceFactory")
|
||||
|
||||
useJUnitPlatform()
|
||||
|
||||
// since we test external backends which we might not control,
|
||||
// we ignore test failures in this module
|
||||
ignoreFailures = true
|
||||
}
|
||||
|
344
external-sop/src/main/kotlin/sop/external/ExternalSOP.kt
vendored
Normal file
344
external-sop/src/main/kotlin/sop/external/ExternalSOP.kt
vendored
Normal file
|
@ -0,0 +1,344 @@
|
|||
// 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)
|
||||
|
||||
override fun updateKey(): UpdateKey = UpdateKeyExternal(binaryName, properties)
|
||||
|
||||
override fun mergeCerts(): MergeCerts = MergeCertsExternal(binaryName, properties)
|
||||
|
||||
override fun certifyUserId(): CertifyUserId = CertifyUserIdExternal(binaryName, properties)
|
||||
|
||||
override fun validateUserId(): ValidateUserId = ValidateUserIdExternal(binaryName, properties)
|
||||
|
||||
/**
|
||||
* This interface can be used to provide a directory in which external SOP binaries can
|
||||
* temporarily store additional results of OpenPGP operations such that the binding classes can
|
||||
* parse them out from there. Unfortunately, on Java you cannot open
|
||||
* [FileDescriptors][java.io.FileDescriptor] arbitrarily, so we have to rely on temporary files
|
||||
* to pass results. An example: `sop decrypt` can emit signature verifications via
|
||||
* `--verify-out=/path/to/tempfile`. [DecryptExternal] will then parse the temp file to make the
|
||||
* result available to consumers. Temporary files are deleted after being read, yet creating
|
||||
* temp files for sensitive information on disk might pose a security risk. Use with care!
|
||||
*/
|
||||
fun interface TempDirProvider {
|
||||
|
||||
@Throws(IOException::class) fun provideTempDirectory(): File
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
@Throws(IOException::class)
|
||||
fun finish(process: Process) {
|
||||
try {
|
||||
mapExitCodeOrException(process)
|
||||
} catch (e: InterruptedException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(InterruptedException::class, IOException::class)
|
||||
private fun mapExitCodeOrException(process: Process) {
|
||||
// wait for process termination
|
||||
val exitCode = process.waitFor()
|
||||
|
||||
if (exitCode == 0) {
|
||||
// we're good, bye
|
||||
return
|
||||
}
|
||||
|
||||
// Read error message
|
||||
val errIn = process.errorStream
|
||||
val errorMessage = readString(errIn)
|
||||
|
||||
when (exitCode) {
|
||||
UnspecificFailure.EXIT_CODE ->
|
||||
throw UnspecificFailure(
|
||||
"External SOP backend reported an unspecific error ($exitCode):\n$errorMessage")
|
||||
NoSignature.EXIT_CODE ->
|
||||
throw NoSignature(
|
||||
"External SOP backend reported error NoSignature ($exitCode):\n$errorMessage")
|
||||
UnsupportedAsymmetricAlgo.EXIT_CODE ->
|
||||
throw UnsupportedOperationException(
|
||||
"External SOP backend reported error UnsupportedAsymmetricAlgo ($exitCode):\n$errorMessage")
|
||||
CertCannotEncrypt.EXIT_CODE ->
|
||||
throw CertCannotEncrypt(
|
||||
"External SOP backend reported error CertCannotEncrypt ($exitCode):\n$errorMessage")
|
||||
MissingArg.EXIT_CODE ->
|
||||
throw MissingArg(
|
||||
"External SOP backend reported error MissingArg ($exitCode):\n$errorMessage")
|
||||
IncompleteVerification.EXIT_CODE ->
|
||||
throw IncompleteVerification(
|
||||
"External SOP backend reported error IncompleteVerification ($exitCode):\n$errorMessage")
|
||||
CannotDecrypt.EXIT_CODE ->
|
||||
throw CannotDecrypt(
|
||||
"External SOP backend reported error CannotDecrypt ($exitCode):\n$errorMessage")
|
||||
PasswordNotHumanReadable.EXIT_CODE ->
|
||||
throw PasswordNotHumanReadable(
|
||||
"External SOP backend reported error PasswordNotHumanReadable ($exitCode):\n$errorMessage")
|
||||
UnsupportedOption.EXIT_CODE ->
|
||||
throw UnsupportedOption(
|
||||
"External SOP backend reported error UnsupportedOption ($exitCode):\n$errorMessage")
|
||||
BadData.EXIT_CODE ->
|
||||
throw BadData(
|
||||
"External SOP backend reported error BadData ($exitCode):\n$errorMessage")
|
||||
ExpectedText.EXIT_CODE ->
|
||||
throw ExpectedText(
|
||||
"External SOP backend reported error ExpectedText ($exitCode):\n$errorMessage")
|
||||
OutputExists.EXIT_CODE ->
|
||||
throw OutputExists(
|
||||
"External SOP backend reported error OutputExists ($exitCode):\n$errorMessage")
|
||||
MissingInput.EXIT_CODE ->
|
||||
throw MissingInput(
|
||||
"External SOP backend reported error MissingInput ($exitCode):\n$errorMessage")
|
||||
KeyIsProtected.EXIT_CODE ->
|
||||
throw KeyIsProtected(
|
||||
"External SOP backend reported error KeyIsProtected ($exitCode):\n$errorMessage")
|
||||
UnsupportedSubcommand.EXIT_CODE ->
|
||||
throw UnsupportedSubcommand(
|
||||
"External SOP backend reported error UnsupportedSubcommand ($exitCode):\n$errorMessage")
|
||||
UnsupportedSpecialPrefix.EXIT_CODE ->
|
||||
throw UnsupportedSpecialPrefix(
|
||||
"External SOP backend reported error UnsupportedSpecialPrefix ($exitCode):\n$errorMessage")
|
||||
AmbiguousInput.EXIT_CODE ->
|
||||
throw AmbiguousInput(
|
||||
"External SOP backend reported error AmbiguousInput ($exitCode):\n$errorMessage")
|
||||
KeyCannotSign.EXIT_CODE ->
|
||||
throw KeyCannotSign(
|
||||
"External SOP backend reported error KeyCannotSign ($exitCode):\n$errorMessage")
|
||||
IncompatibleOptions.EXIT_CODE ->
|
||||
throw IncompatibleOptions(
|
||||
"External SOP backend reported error IncompatibleOptions ($exitCode):\n$errorMessage")
|
||||
UnsupportedProfile.EXIT_CODE ->
|
||||
throw UnsupportedProfile(
|
||||
"External SOP backend reported error UnsupportedProfile ($exitCode):\n$errorMessage")
|
||||
NoHardwareKeyFound.EXIT_CODE ->
|
||||
throw NoHardwareKeyFound(
|
||||
"External SOP backend reported error NoHardwareKeyFound ($exitCode):\n$errorMessage")
|
||||
HardwareKeyFailure.EXIT_CODE ->
|
||||
throw HardwareKeyFailure(
|
||||
"External SOP backend reported error HardwareKeyFailure ($exitCode):\n$errorMessage")
|
||||
PrimaryKeyBad.EXIT_CODE ->
|
||||
throw PrimaryKeyBad(
|
||||
"External SOP backend reported error PrimaryKeyBad ($exitCode):\n$errorMessage")
|
||||
CertUserIdNoMatch.EXIT_CODE ->
|
||||
throw CertUserIdNoMatch(
|
||||
"External SOP backend reported error CertUserIdNoMatch ($exitCode):\n$errorMessage")
|
||||
KeyCannotCertify.EXIT_CODE ->
|
||||
throw KeyCannotCertify(
|
||||
"External SOP backend reported error KeyCannotCertify ($exitCode):\n$errorMessage")
|
||||
|
||||
// Did you forget to add a case for a new exception type?
|
||||
else ->
|
||||
throw RuntimeException(
|
||||
"External SOP backend reported unknown exit code ($exitCode):\n$errorMessage")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all key-value pairs from the given [Properties] object as a list with items of the
|
||||
* form `key=value`.
|
||||
*
|
||||
* @param properties properties
|
||||
* @return list of key=value strings
|
||||
*/
|
||||
@JvmStatic
|
||||
fun propertiesToEnv(properties: Properties): List<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() }
|
||||
}
|
||||
}
|
||||
}
|
57
external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt
vendored
Normal file
57
external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt
vendored
Normal file
|
@ -0,0 +1,57 @@
|
|||
// 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.ValidateUserIdExternal
|
||||
import sop.external.operation.VersionExternal
|
||||
import sop.operation.DetachedVerify
|
||||
import sop.operation.InlineVerify
|
||||
import sop.operation.ValidateUserId
|
||||
import sop.operation.Version
|
||||
|
||||
/**
|
||||
* Implementation of the [SOPV] API subset using an external sopv/sop binary.
|
||||
*
|
||||
* Instantiate an [ExternalSOPV] object for the given binary and the given [TempDirProvider] using
|
||||
* empty environment variables.
|
||||
*
|
||||
* @param binaryName name / path of the sopv binary
|
||||
* @param tempDirProvider custom tempDirProvider
|
||||
*/
|
||||
class ExternalSOPV(
|
||||
private val binaryName: String,
|
||||
private val properties: Properties = Properties(),
|
||||
private val tempDirProvider: TempDirProvider = defaultTempDirProvider()
|
||||
) : SOPV {
|
||||
|
||||
override fun version(): Version = VersionExternal(binaryName, properties)
|
||||
|
||||
override fun detachedVerify(): DetachedVerify = DetachedVerifyExternal(binaryName, properties)
|
||||
|
||||
override fun inlineVerify(): InlineVerify =
|
||||
InlineVerifyExternal(binaryName, properties, tempDirProvider)
|
||||
|
||||
override fun validateUserId(): ValidateUserId = ValidateUserIdExternal(binaryName, properties)
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Default implementation of the [TempDirProvider] which stores temporary files in the
|
||||
* systems temp dir ([Files.createTempDirectory]).
|
||||
*
|
||||
* @return default implementation
|
||||
*/
|
||||
@JvmStatic
|
||||
fun defaultTempDirProvider(): TempDirProvider {
|
||||
return TempDirProvider { Files.createTempDirectory("ext-sopv").toFile() }
|
||||
}
|
||||
}
|
||||
}
|
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)
|
||||
}
|
48
external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt
vendored
Normal file
48
external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.external.operation
|
||||
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import sop.Ready
|
||||
import sop.external.ExternalSOP
|
||||
import sop.operation.CertifyUserId
|
||||
|
||||
class CertifyUserIdExternal(binary: String, environment: Properties) : CertifyUserId {
|
||||
|
||||
private val commandList = mutableListOf(binary, "certify-userid")
|
||||
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
|
||||
|
||||
private var argCount = 0
|
||||
|
||||
private val keys: MutableList<String> = mutableListOf()
|
||||
|
||||
override fun noArmor(): CertifyUserId = apply { commandList.add("--no-armor") }
|
||||
|
||||
override fun userId(userId: String): CertifyUserId = apply {
|
||||
commandList.add("--userid")
|
||||
commandList.add(userId)
|
||||
}
|
||||
|
||||
override fun withKeyPassword(password: ByteArray): CertifyUserId = apply {
|
||||
commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount")
|
||||
envList.add("KEY_PASSWORD_$argCount=${String(password)}")
|
||||
argCount += 1
|
||||
}
|
||||
|
||||
override fun noRequireSelfSig(): CertifyUserId = apply {
|
||||
commandList.add("--no-require-self-sig")
|
||||
}
|
||||
|
||||
override fun keys(keys: InputStream): CertifyUserId = apply {
|
||||
this.keys.add("@ENV:KEY_$argCount")
|
||||
envList.add("KEY_$argCount=${ExternalSOP.readString(keys)}")
|
||||
argCount += 1
|
||||
}
|
||||
|
||||
override fun certs(certs: InputStream): Ready =
|
||||
ExternalSOP.executeTransformingOperation(
|
||||
Runtime.getRuntime(), commandList.plus("--").plus(keys), envList, certs)
|
||||
}
|
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("--verifications-out=${verifyOut.absolutePath}")
|
||||
}
|
||||
|
||||
try {
|
||||
val process =
|
||||
Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray())
|
||||
val processOut = process.outputStream
|
||||
val processIn = process.inputStream
|
||||
|
||||
return object : ReadyWithResult<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=${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) }
|
||||
}
|
||||
}
|
30
external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt
vendored
Normal file
30
external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.external.operation
|
||||
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import sop.Ready
|
||||
import sop.external.ExternalSOP
|
||||
import sop.operation.MergeCerts
|
||||
|
||||
class MergeCertsExternal(binary: String, environment: Properties) : MergeCerts {
|
||||
|
||||
private val commandList = mutableListOf(binary, "merge-certs")
|
||||
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
|
||||
|
||||
private var argCount = 0
|
||||
|
||||
override fun noArmor(): MergeCerts = apply { commandList.add("--no-armor") }
|
||||
|
||||
override fun updates(updateCerts: InputStream): MergeCerts = apply {
|
||||
commandList.add("@ENV:CERT_$argCount")
|
||||
envList.add("CERT_$argCount=${ExternalSOP.readString(updateCerts)}")
|
||||
argCount += 1
|
||||
}
|
||||
|
||||
override fun baseCertificates(certs: InputStream): Ready =
|
||||
ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, certs)
|
||||
}
|
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)
|
||||
}
|
43
external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt
vendored
Normal file
43
external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.external.operation
|
||||
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import sop.Ready
|
||||
import sop.external.ExternalSOP
|
||||
import sop.operation.UpdateKey
|
||||
|
||||
class UpdateKeyExternal(binary: String, environment: Properties) : UpdateKey {
|
||||
|
||||
private val commandList = mutableListOf(binary, "update-key")
|
||||
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
|
||||
|
||||
private var argCount = 0
|
||||
|
||||
override fun noArmor(): UpdateKey = apply { commandList.add("--no-armor") }
|
||||
|
||||
override fun signingOnly(): UpdateKey = apply { commandList.add("--signing-only") }
|
||||
|
||||
override fun noAddedCapabilities(): UpdateKey = apply {
|
||||
commandList.add("--no-added-capabilities")
|
||||
}
|
||||
|
||||
override fun withKeyPassword(password: ByteArray): UpdateKey = apply {
|
||||
commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount")
|
||||
envList.add("KEY_PASSWORD_$argCount=${String(password)}")
|
||||
argCount += 1
|
||||
}
|
||||
|
||||
override fun mergeCerts(certs: InputStream): UpdateKey = apply {
|
||||
commandList.add("--merge-certs")
|
||||
commandList.add("@ENV:CERT_$argCount")
|
||||
envList.add("CERT_$argCount=${ExternalSOP.readString(certs)}")
|
||||
argCount += 1
|
||||
}
|
||||
|
||||
override fun key(key: InputStream): Ready =
|
||||
ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, key)
|
||||
}
|
43
external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt
vendored
Normal file
43
external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.external.operation
|
||||
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import sop.external.ExternalSOP
|
||||
import sop.operation.ValidateUserId
|
||||
import sop.util.UTCUtil
|
||||
|
||||
class ValidateUserIdExternal(binary: String, environment: Properties) : ValidateUserId {
|
||||
|
||||
private val commandList = mutableListOf(binary, "validate-userid")
|
||||
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
|
||||
|
||||
private var argCount = 0
|
||||
|
||||
private var userId: String? = null
|
||||
private val authorities: MutableList<String> = mutableListOf()
|
||||
|
||||
override fun addrSpecOnly(): ValidateUserId = apply { commandList.add("--addr-spec-only") }
|
||||
|
||||
override fun userId(userId: String): ValidateUserId = apply { this.userId = userId }
|
||||
|
||||
override fun authorities(certs: InputStream): ValidateUserId = apply {
|
||||
this.authorities.add("@ENV:CERT_$argCount")
|
||||
envList.add("CERT_$argCount=${ExternalSOP.readString(certs)}")
|
||||
argCount += 1
|
||||
}
|
||||
|
||||
override fun subjects(certs: InputStream): Boolean {
|
||||
ExternalSOP.executeTransformingOperation(
|
||||
Runtime.getRuntime(), commandList.plus(userId!!).plus(authorities), envList, certs)
|
||||
.bytes
|
||||
return true
|
||||
}
|
||||
|
||||
override fun validateAt(date: Date): ValidateUserId = apply {
|
||||
commandList.add("--validate-at=${UTCUtil.formatUTCDate(date)}")
|
||||
}
|
||||
}
|
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)
|
||||
}
|
||||
}
|
||||
}
|
5
external-sop/src/main/resources/sop/testsuite/external/.gitignore
vendored
Normal file
5
external-sop/src/main/resources/sop/testsuite/external/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
# SPDX-FileCopyrightText: 2023 Paul Schaub <info@pgpainless.org>
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
config.json
|
8
external-sop/src/main/resources/sop/testsuite/external/config.json.ci
vendored
Normal file
8
external-sop/src/main/resources/sop/testsuite/external/config.json.ci
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"backends": [
|
||||
{
|
||||
"name": "Sequoia-SOP",
|
||||
"sop": "/usr/bin/sqop"
|
||||
}
|
||||
]
|
||||
}
|
17
external-sop/src/main/resources/sop/testsuite/external/config.json.example
vendored
Normal file
17
external-sop/src/main/resources/sop/testsuite/external/config.json.example
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"backends": [
|
||||
{
|
||||
"name": "Example-SOP",
|
||||
"sop": "/usr/bin/example-sop"
|
||||
},
|
||||
{
|
||||
"name": "Awesome-SOP",
|
||||
"sop": "/usr/local/bin/awesome-sop",
|
||||
"env": [
|
||||
{
|
||||
"key": "myEnvironmentVariable", "value": "FooBar"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
80
external-sop/src/test/java/sop/testsuite/external/ExternalSOPInstanceFactory.java
vendored
Normal file
80
external-sop/src/test/java/sop/testsuite/external/ExternalSOPInstanceFactory.java
vendored
Normal file
|
@ -0,0 +1,80 @@
|
|||
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.testsuite.external;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import sop.SOP;
|
||||
import sop.external.ExternalSOP;
|
||||
import sop.testsuite.SOPInstanceFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* This implementation of {@link SOPInstanceFactory} reads the JSON file at
|
||||
* <pre>external-sop/src/main/resources/sop/testsuite/external/config.json</pre>
|
||||
* to determine configured external test backends.
|
||||
*/
|
||||
public class ExternalSOPInstanceFactory extends SOPInstanceFactory {
|
||||
|
||||
@Override
|
||||
public Map<String, SOP> provideSOPInstances() {
|
||||
Map<String, SOP> backends = new HashMap<>();
|
||||
TestSuite suite = readConfiguration();
|
||||
if (suite != null && !suite.backends.isEmpty()) {
|
||||
for (TestSubject subject : suite.backends) {
|
||||
if (!new File(subject.sop).exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Properties env = new Properties();
|
||||
if (subject.env != null) {
|
||||
for (Var var : subject.env) {
|
||||
env.put(var.key, var.value);
|
||||
}
|
||||
}
|
||||
|
||||
SOP sop = new ExternalSOP(subject.sop, env);
|
||||
backends.put(subject.name, sop);
|
||||
}
|
||||
}
|
||||
return backends;
|
||||
}
|
||||
|
||||
|
||||
public static TestSuite readConfiguration() {
|
||||
Gson gson = new Gson();
|
||||
InputStream inputStream = ExternalSOPInstanceFactory.class.getResourceAsStream("config.json");
|
||||
if (inputStream == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InputStreamReader reader = new InputStreamReader(inputStream);
|
||||
return gson.fromJson(reader, TestSuite.class);
|
||||
}
|
||||
|
||||
|
||||
// JSON DTOs
|
||||
|
||||
public static class TestSuite {
|
||||
List<TestSubject> backends;
|
||||
}
|
||||
|
||||
public static class TestSubject {
|
||||
String name;
|
||||
String sop;
|
||||
List<Var> env;
|
||||
}
|
||||
|
||||
public static class Var {
|
||||
String key;
|
||||
String value;
|
||||
}
|
||||
}
|
18
external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java
vendored
Normal file
18
external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.testsuite.external;
|
||||
|
||||
import org.junit.platform.suite.api.IncludeClassNamePatterns;
|
||||
import org.junit.platform.suite.api.SelectPackages;
|
||||
import org.junit.platform.suite.api.Suite;
|
||||
import org.junit.platform.suite.api.SuiteDisplayName;
|
||||
|
||||
@Suite
|
||||
@SuiteDisplayName("External SOP Tests")
|
||||
@SelectPackages("sop.testsuite.operation")
|
||||
@IncludeClassNamePatterns(".*Test")
|
||||
public class ExternalTestSuite {
|
||||
|
||||
}
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,5 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
269
gradlew
vendored
269
gradlew
vendored
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -17,67 +17,101 @@
|
|||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MSYS* | MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
|
|||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
|
@ -106,80 +140,95 @@ location of your Java installation."
|
|||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
|
|
@ -2,8 +2,11 @@
|
|||
//
|
||||
// SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
rootProject.name = 'PGPainless'
|
||||
rootProject.name = 'SOP-Java'
|
||||
|
||||
include 'sop-java',
|
||||
'sop-java-picocli'
|
||||
'sop-java-picocli',
|
||||
'sop-java-testfixtures',
|
||||
'external-sop',
|
||||
'sop-java-json-gson'
|
||||
|
||||
|
|
13
sop-java-json-gson/README.md
Normal file
13
sop-java-json-gson/README.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
# SOP-Java-JSON-GSON
|
||||
|
||||
## JSON Parsing VERIFICATION extension JSON using Gson
|
||||
|
||||
Since revision 11, the SOP specification defines VERIFICATIONS extension JSON.
|
||||
|
||||
This module implements the `JSONParser` and `JSONSerializer` interfaces using Googles Gson library.
|
28
sop-java-json-gson/build.gradle
Normal file
28
sop-java-json-gson/build.gradle
Normal file
|
@ -0,0 +1,28 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
plugins {
|
||||
id 'java-library'
|
||||
}
|
||||
|
||||
group 'org.pgpainless'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||
implementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
|
||||
runtimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||
|
||||
implementation project(":sop-java")
|
||||
api "org.slf4j:slf4j-api:$slf4jVersion"
|
||||
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
|
||||
|
||||
// @Nonnull, @Nullable...
|
||||
implementation "com.google.code.findbugs:jsr305:$jsrVersion"
|
||||
|
||||
api "com.google.code.gson:gson:$gsonVersion"
|
||||
}
|
23
sop-java-json-gson/src/main/kotlin/sop/GsonParser.kt
Normal file
23
sop-java-json-gson/src/main/kotlin/sop/GsonParser.kt
Normal file
|
@ -0,0 +1,23 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.text.ParseException
|
||||
|
||||
class GsonParser(
|
||||
private val gson: Gson = Gson()
|
||||
) : Verification.JSONParser {
|
||||
|
||||
override fun parse(string: String): Verification.JSON {
|
||||
try {
|
||||
return gson.fromJson(string, object : TypeToken<Verification.JSON>(){}.type)
|
||||
} catch (e: JsonSyntaxException) {
|
||||
throw ParseException(e.message, 0)
|
||||
}
|
||||
}
|
||||
}
|
16
sop-java-json-gson/src/main/kotlin/sop/GsonSerializer.kt
Normal file
16
sop-java-json-gson/src/main/kotlin/sop/GsonSerializer.kt
Normal file
|
@ -0,0 +1,16 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop
|
||||
|
||||
import com.google.gson.Gson
|
||||
|
||||
class GsonSerializer(
|
||||
private val gson: Gson = Gson()
|
||||
) : Verification.JSONSerializer {
|
||||
|
||||
override fun serialize(json: Verification.JSON): String {
|
||||
return gson.toJson(json)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import java.text.ParseException
|
||||
|
||||
class GsonSerializerAndParserTest {
|
||||
|
||||
private val serializer: GsonSerializer = GsonSerializer()
|
||||
private val parser: GsonParser = GsonParser()
|
||||
|
||||
@Test
|
||||
fun simpleSingleTest() {
|
||||
val before = Verification.JSON("/tmp/alice.pgp")
|
||||
|
||||
val json = serializer.serialize(before)
|
||||
assertEquals("{\"signers\":[\"/tmp/alice.pgp\"]}", json)
|
||||
|
||||
val after = parser.parse(json)
|
||||
|
||||
assertEquals(before, after)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun simpleListTest() {
|
||||
val before = Verification.JSON(listOf("/tmp/alice.pgp", "/tmp/bob.asc"))
|
||||
|
||||
val json = serializer.serialize(before)
|
||||
assertEquals("{\"signers\":[\"/tmp/alice.pgp\",\"/tmp/bob.asc\"]}", json)
|
||||
|
||||
val after = parser.parse(json)
|
||||
|
||||
assertEquals(before, after)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun withCommentTest() {
|
||||
val before = Verification.JSON(
|
||||
listOf("/tmp/alice.pgp"),
|
||||
"This is a comment.",
|
||||
null)
|
||||
|
||||
val json = serializer.serialize(before)
|
||||
assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\"}", json)
|
||||
|
||||
val after = parser.parse(json)
|
||||
|
||||
assertEquals(before, after)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun withExtStringTest() {
|
||||
val before = Verification.JSON(
|
||||
listOf("/tmp/alice.pgp"),
|
||||
"This is a comment.",
|
||||
"This is an ext object string.")
|
||||
|
||||
val json = serializer.serialize(before)
|
||||
assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\",\"ext\":\"This is an ext object string.\"}", json)
|
||||
|
||||
val after = parser.parse(json)
|
||||
|
||||
assertEquals(before, after)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun withExtListTest() {
|
||||
val before = Verification.JSON(
|
||||
listOf("/tmp/alice.pgp"),
|
||||
"This is a comment.",
|
||||
listOf(1.0,2.0,3.0))
|
||||
|
||||
val json = serializer.serialize(before)
|
||||
assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\",\"ext\":[1.0,2.0,3.0]}", json)
|
||||
|
||||
val after = parser.parse(json)
|
||||
|
||||
assertEquals(before, after)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseInvalidJSON() {
|
||||
assertThrows<ParseException> { parser.parse("Invalid") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseMalformedJSON() {
|
||||
// Missing '}'
|
||||
assertThrows<ParseException> { parser.parse("{\"signers\":[\"Alice\"]") }
|
||||
}
|
||||
}
|
|
@ -5,7 +5,10 @@ SPDX-License-Identifier: Apache-2.0
|
|||
-->
|
||||
# SOP-Java-Picocli
|
||||
|
||||
Implementation of the [Stateless OpenPGP Command Line Interface](https://tools.ietf.org/html/draft-dkg-openpgp-stateless-cli-01) specification.
|
||||
[](https://javadoc.io/doc/org.pgpainless/sop-java-picocli)
|
||||
[](https://search.maven.org/artifact/org.pgpainless/sop-java-picocli)
|
||||
|
||||
Implementation of the [Stateless OpenPGP Command Line Interface](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) specification.
|
||||
This terminal application allows generation of OpenPGP keys, extraction of public key certificates,
|
||||
armoring and de-armoring of data, as well as - of course - encryption/decryption of messages and creation/verification of signatures.
|
||||
|
||||
|
@ -31,4 +34,4 @@ java -jar sop-java-picocli-XXX.jar help
|
|||
If you just want to get started encrypting messages, see the module `pgpainless-cli` which initializes
|
||||
`sop-java-picocli` with `pgpainless-sop`, so you can get started right away without the need to manually wire stuff up.
|
||||
|
||||
Enjoy!
|
||||
Enjoy!
|
||||
|
|
|
@ -4,26 +4,43 @@
|
|||
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'org.asciidoctor.jvm.convert' version '3.3.2'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// JUnit
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
|
||||
|
||||
// https://todd.ginsberg.com/post/testing-system-exit/
|
||||
testImplementation 'com.ginsberg:junit5-system-exit:1.1.1'
|
||||
testImplementation 'org.mockito:mockito-core:4.2.0'
|
||||
// Mocking Components
|
||||
testImplementation "org.mockito:mockito-core:$mockitoVersion"
|
||||
|
||||
// SOP
|
||||
implementation(project(":sop-java"))
|
||||
implementation "info.picocli:picocli:$picocliVersion"
|
||||
testImplementation(project(":sop-java-testfixtures"))
|
||||
|
||||
// https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305
|
||||
implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
|
||||
// CLI
|
||||
implementation "info.picocli:picocli:$picocliVersion"
|
||||
kapt "info.picocli:picocli-codegen:$picocliVersion"
|
||||
|
||||
// @Nonnull, @Nullable...
|
||||
implementation "com.google.code.findbugs:jsr305:$jsrVersion"
|
||||
}
|
||||
|
||||
mainClassName = 'sop.cli.picocli.SopCLI'
|
||||
|
||||
application {
|
||||
mainClass = mainClassName
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.compilerArgs += ["-Aproject=${project.group}/${project.name}"]
|
||||
}
|
||||
|
||||
jar {
|
||||
dependsOn(":sop-java:jar")
|
||||
duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
|
||||
|
||||
manifest {
|
||||
attributes 'Main-Class': "$mainClassName"
|
||||
}
|
||||
|
@ -37,3 +54,24 @@ jar {
|
|||
}
|
||||
}
|
||||
|
||||
task generateManpageAsciiDoc(type: JavaExec) {
|
||||
dependsOn(classes)
|
||||
group = "Documentation"
|
||||
description = "Generate AsciiDoc manpage"
|
||||
classpath(configurations.annotationProcessor, sourceSets.main.runtimeClasspath)
|
||||
systemProperty("user.language", "en")
|
||||
main 'picocli.codegen.docgen.manpage.ManPageGenerator'
|
||||
args mainClassName, "--outdir=${project.buildDir}/generated-picocli-docs", "-v" //, "--template-dir=src/docs/mantemplates"
|
||||
}
|
||||
|
||||
apply plugin: 'org.asciidoctor.jvm.convert'
|
||||
asciidoctor {
|
||||
attributes 'reproducible': ''
|
||||
dependsOn(generateManpageAsciiDoc)
|
||||
sourceDir = file("${project.buildDir}/generated-picocli-docs")
|
||||
outputDir = file("${project.buildDir}/docs")
|
||||
logDocuments = true
|
||||
outputOptions {
|
||||
backends = ['manpage', 'html5']
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.cli.picocli;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import sop.util.UTCUtil;
|
||||
|
||||
public class DateParser {
|
||||
|
||||
public static final Date BEGINNING_OF_TIME = new Date(0);
|
||||
public static final Date END_OF_TIME = new Date(8640000000000000L);
|
||||
|
||||
public static Date parseNotAfter(String notAfter) {
|
||||
Date date = notAfter.equals("now") ? new Date() : notAfter.equals("-") ? END_OF_TIME : UTCUtil.parseUTCDate(notAfter);
|
||||
if (date == null) {
|
||||
Print.errln("Invalid date string supplied as value of --not-after.");
|
||||
System.exit(1);
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
public static Date parseNotBefore(String notBefore) {
|
||||
Date date = notBefore.equals("now") ? new Date() : notBefore.equals("-") ? BEGINNING_OF_TIME : UTCUtil.parseUTCDate(notBefore);
|
||||
if (date == null) {
|
||||
Print.errln("Invalid date string supplied as value of --not-before.");
|
||||
System.exit(1);
|
||||
}
|
||||
return date;
|
||||
}
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.cli.picocli;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
import sop.exception.SOPGPException;
|
||||
|
||||
public class FileUtil {
|
||||
|
||||
private static final String ERROR_AMBIGUOUS = "File name '%s' is ambiguous. File with the same name exists on the filesystem.";
|
||||
private static final String ERROR_ENV_FOUND = "Environment variable '%s' not set.";
|
||||
private static final String ERROR_OUTPUT_EXISTS = "Output file '%s' already exists.";
|
||||
private static final String ERROR_INPUT_NOT_EXIST = "File '%s' does not exist.";
|
||||
private static final String ERROR_CANNOT_CREATE_FILE = "Output file '%s' cannot be created: %s";
|
||||
|
||||
public static final String PRFX_ENV = "@ENV:";
|
||||
public static final String PRFX_FD = "@FD:";
|
||||
|
||||
private static EnvironmentVariableResolver envResolver = System::getenv;
|
||||
|
||||
public static void setEnvironmentVariableResolver(EnvironmentVariableResolver envResolver) {
|
||||
if (envResolver == null) {
|
||||
throw new NullPointerException("Variable envResolver cannot be null.");
|
||||
}
|
||||
FileUtil.envResolver = envResolver;
|
||||
}
|
||||
|
||||
public interface EnvironmentVariableResolver {
|
||||
/**
|
||||
* Resolve the value of the given environment variable.
|
||||
* Return null if the variable is not present.
|
||||
*
|
||||
* @param name name of the variable
|
||||
* @return variable value or null
|
||||
*/
|
||||
String resolveEnvironmentVariable(String name);
|
||||
}
|
||||
|
||||
public static File getFile(String fileName) {
|
||||
if (fileName == null) {
|
||||
throw new NullPointerException("File name cannot be null.");
|
||||
}
|
||||
|
||||
if (fileName.startsWith(PRFX_ENV)) {
|
||||
|
||||
if (new File(fileName).exists()) {
|
||||
throw new SOPGPException.AmbiguousInput(String.format(ERROR_AMBIGUOUS, fileName));
|
||||
}
|
||||
|
||||
String envName = fileName.substring(PRFX_ENV.length());
|
||||
String envValue = envResolver.resolveEnvironmentVariable(envName);
|
||||
if (envValue == null) {
|
||||
throw new IllegalArgumentException(String.format(ERROR_ENV_FOUND, envName));
|
||||
}
|
||||
return new File(envValue);
|
||||
} else if (fileName.startsWith(PRFX_FD)) {
|
||||
|
||||
if (new File(fileName).exists()) {
|
||||
throw new SOPGPException.AmbiguousInput(String.format(ERROR_AMBIGUOUS, fileName));
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("File descriptors not supported.");
|
||||
}
|
||||
|
||||
return new File(fileName);
|
||||
}
|
||||
|
||||
public static FileInputStream getFileInputStream(String fileName) {
|
||||
File file = getFile(fileName);
|
||||
try {
|
||||
FileInputStream inputStream = new FileInputStream(file);
|
||||
return inputStream;
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new SOPGPException.MissingInput(String.format(ERROR_INPUT_NOT_EXIST, fileName), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static File createNewFileOrThrow(File file) throws IOException {
|
||||
if (file == null) {
|
||||
throw new NullPointerException("File cannot be null.");
|
||||
}
|
||||
|
||||
try {
|
||||
if (!file.createNewFile()) {
|
||||
throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_EXISTS, file.getAbsolutePath()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IOException(String.format(ERROR_CANNOT_CREATE_FILE, file.getAbsolutePath(), e.getMessage()));
|
||||
}
|
||||
return file;
|
||||
}
|
||||
}
|
|
@ -1,26 +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 errln(String string) {
|
||||
// CHECKSTYLE:OFF
|
||||
System.err.println(string);
|
||||
// CHECKSTYLE:ON
|
||||
}
|
||||
|
||||
public static void trace(Throwable e) {
|
||||
// CHECKSTYLE:OFF
|
||||
e.printStackTrace();
|
||||
// CHECKSTYLE:ON
|
||||
}
|
||||
|
||||
public static void outln(String string) {
|
||||
// CHECKSTYLE:OFF
|
||||
System.out.println(string);
|
||||
// CHECKSTYLE:ON
|
||||
}
|
||||
}
|
|
@ -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,26 +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()));
|
||||
}
|
||||
ex.printStackTrace(commandLine.getErr());
|
||||
// CHECKSTYLE:ON
|
||||
|
||||
return exitCode;
|
||||
}
|
||||
}
|
|
@ -1,68 +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.SOP;
|
||||
import sop.cli.picocli.commands.ArmorCmd;
|
||||
import sop.cli.picocli.commands.DearmorCmd;
|
||||
import sop.cli.picocli.commands.DecryptCmd;
|
||||
import sop.cli.picocli.commands.DetachInbandSignatureAndMessageCmd;
|
||||
import sop.cli.picocli.commands.EncryptCmd;
|
||||
import sop.cli.picocli.commands.ExtractCertCmd;
|
||||
import sop.cli.picocli.commands.GenerateKeyCmd;
|
||||
import sop.cli.picocli.commands.SignCmd;
|
||||
import sop.cli.picocli.commands.VerifyCmd;
|
||||
import sop.cli.picocli.commands.VersionCmd;
|
||||
|
||||
@CommandLine.Command(
|
||||
exitCodeOnInvalidInput = 69,
|
||||
subcommands = {
|
||||
CommandLine.HelpCommand.class,
|
||||
ArmorCmd.class,
|
||||
DearmorCmd.class,
|
||||
DecryptCmd.class,
|
||||
DetachInbandSignatureAndMessageCmd.class,
|
||||
EncryptCmd.class,
|
||||
ExtractCertCmd.class,
|
||||
GenerateKeyCmd.class,
|
||||
SignCmd.class,
|
||||
VerifyCmd.class,
|
||||
VersionCmd.class
|
||||
}
|
||||
)
|
||||
public class SopCLI {
|
||||
// Singleton
|
||||
static SOP SOP_INSTANCE;
|
||||
|
||||
public static String EXECUTABLE_NAME = "sop";
|
||||
|
||||
public static void main(String[] args) {
|
||||
int exitCode = execute(args);
|
||||
if (exitCode != 0) {
|
||||
System.exit(exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
public static int execute(String[] args) {
|
||||
return new CommandLine(SopCLI.class)
|
||||
.setCommandName(EXECUTABLE_NAME)
|
||||
.setExecutionExceptionHandler(new SOPExecutionExceptionHandler())
|
||||
.setExitCodeExceptionMapper(new SOPExceptionExitCodeMapper())
|
||||
.setCaseInsensitiveEnumValuesAllowed(true)
|
||||
.execute(args);
|
||||
}
|
||||
|
||||
public static SOP getSop() {
|
||||
if (SOP_INSTANCE == null) {
|
||||
throw new IllegalStateException("No SOP backend set.");
|
||||
}
|
||||
return SOP_INSTANCE;
|
||||
}
|
||||
|
||||
public static void setSopInstance(SOP instance) {
|
||||
SOP_INSTANCE = instance;
|
||||
}
|
||||
}
|
|
@ -1,54 +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.Print;
|
||||
import sop.cli.picocli.SopCLI;
|
||||
import sop.enums.ArmorLabel;
|
||||
import sop.exception.SOPGPException;
|
||||
import sop.operation.Armor;
|
||||
|
||||
@CommandLine.Command(name = "armor",
|
||||
description = "Add ASCII Armor to standard input",
|
||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
||||
public class ArmorCmd implements Runnable {
|
||||
|
||||
@CommandLine.Option(names = {"--label"}, description = "Label to be used in the header and tail of the armoring.", paramLabel = "{auto|sig|key|cert|message}")
|
||||
ArmorLabel label;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Armor armor = SopCLI.getSop().armor();
|
||||
if (armor == null) {
|
||||
throw new SOPGPException.UnsupportedSubcommand("Command 'armor' not implemented.");
|
||||
}
|
||||
|
||||
if (label != null) {
|
||||
try {
|
||||
armor.label(label);
|
||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||
Print.errln("Armor labels not supported.");
|
||||
System.exit(unsupportedOption.getExitCode());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Ready ready = armor.data(System.in);
|
||||
ready.writeTo(System.out);
|
||||
} catch (SOPGPException.BadData badData) {
|
||||
Print.errln("Bad data.");
|
||||
Print.trace(badData);
|
||||
System.exit(badData.getExitCode());
|
||||
} catch (IOException e) {
|
||||
Print.errln("IO Error.");
|
||||
Print.trace(e);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,42 +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.cli.picocli.Print;
|
||||
import sop.cli.picocli.SopCLI;
|
||||
import sop.exception.SOPGPException;
|
||||
import sop.operation.Dearmor;
|
||||
|
||||
@CommandLine.Command(name = "dearmor",
|
||||
description = "Remove ASCII Armor from standard input",
|
||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
||||
public class DearmorCmd implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Dearmor dearmor = SopCLI.getSop().dearmor();
|
||||
if (dearmor == null) {
|
||||
throw new SOPGPException.UnsupportedSubcommand("Command 'dearmor' not implemented.");
|
||||
}
|
||||
|
||||
try {
|
||||
SopCLI.getSop()
|
||||
.dearmor()
|
||||
.data(System.in)
|
||||
.writeTo(System.out);
|
||||
} catch (SOPGPException.BadData e) {
|
||||
Print.errln("Bad data.");
|
||||
Print.trace(e);
|
||||
System.exit(e.getExitCode());
|
||||
} catch (IOException e) {
|
||||
Print.errln("IO Error.");
|
||||
Print.trace(e);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,240 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.cli.picocli.commands;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import sop.DecryptionResult;
|
||||
import sop.ReadyWithResult;
|
||||
import sop.SessionKey;
|
||||
import sop.Verification;
|
||||
import sop.cli.picocli.DateParser;
|
||||
import sop.cli.picocli.FileUtil;
|
||||
import sop.cli.picocli.SopCLI;
|
||||
import sop.exception.SOPGPException;
|
||||
import sop.operation.Decrypt;
|
||||
import sop.util.HexUtil;
|
||||
|
||||
@CommandLine.Command(name = "decrypt",
|
||||
description = "Decrypt a message from standard input",
|
||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
||||
public class DecryptCmd implements Runnable {
|
||||
|
||||
private static final String SESSION_KEY_OUT = "--session-key-out";
|
||||
private static final String VERIFY_OUT = "--verify-out";
|
||||
|
||||
private static final String ERROR_UNSUPPORTED_OPTION = "Option '%s' is not supported.";
|
||||
private static final String ERROR_FILE_NOT_EXIST = "File '%s' does not exist.";
|
||||
private static final String ERROR_OUTPUT_OF_OPTION_EXISTS = "Target %s of option %s already exists.";
|
||||
|
||||
@CommandLine.Option(
|
||||
names = {SESSION_KEY_OUT},
|
||||
description = "Can be used to learn the session key on successful decryption",
|
||||
paramLabel = "SESSIONKEY")
|
||||
File sessionKeyOut;
|
||||
|
||||
@CommandLine.Option(
|
||||
names = {"--with-session-key"},
|
||||
description = "Enables decryption of the \"CIPHERTEXT\" using the session key directly against the \"SEIPD\" packet",
|
||||
paramLabel = "SESSIONKEY")
|
||||
List<String> withSessionKey = new ArrayList<>();
|
||||
|
||||
@CommandLine.Option(
|
||||
names = {"--with-password"},
|
||||
description = "Enables decryption based on any \"SKESK\" packets in the \"CIPHERTEXT\"",
|
||||
paramLabel = "PASSWORD")
|
||||
List<String> withPassword = new ArrayList<>();
|
||||
|
||||
@CommandLine.Option(names = {VERIFY_OUT},
|
||||
description = "Produces signature verification status to the designated file",
|
||||
paramLabel = "VERIFICATIONS")
|
||||
File verifyOut;
|
||||
|
||||
@CommandLine.Option(names = {"--verify-with"},
|
||||
description = "Certificates whose signatures would be acceptable for signatures over this message",
|
||||
paramLabel = "CERT")
|
||||
List<File> certs = new ArrayList<>();
|
||||
|
||||
@CommandLine.Option(names = {"--not-before"},
|
||||
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
|
||||
"Reject signatures with a creation date not in range.\n" +
|
||||
"Defaults to beginning of time (\"-\").",
|
||||
paramLabel = "DATE")
|
||||
String notBefore = "-";
|
||||
|
||||
@CommandLine.Option(names = {"--not-after"},
|
||||
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
|
||||
"Reject signatures with a creation date not in range.\n" +
|
||||
"Defaults to current system time (\"now\").\n" +
|
||||
"Accepts special value \"-\" for end of time.",
|
||||
paramLabel = "DATE")
|
||||
String notAfter = "now";
|
||||
|
||||
@CommandLine.Parameters(index = "0..*",
|
||||
description = "Secret keys to attempt decryption with",
|
||||
paramLabel = "KEY")
|
||||
List<File> keys = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
throwIfOutputExists(verifyOut, VERIFY_OUT);
|
||||
throwIfOutputExists(sessionKeyOut, SESSION_KEY_OUT);
|
||||
|
||||
Decrypt decrypt = SopCLI.getSop().decrypt();
|
||||
if (decrypt == null) {
|
||||
throw new SOPGPException.UnsupportedSubcommand("Command 'decrypt' not implemented.");
|
||||
}
|
||||
|
||||
setNotAfter(notAfter, decrypt);
|
||||
setNotBefore(notBefore, decrypt);
|
||||
setWithPasswords(withPassword, decrypt);
|
||||
setWithSessionKeys(withSessionKey, decrypt);
|
||||
setVerifyWith(certs, decrypt);
|
||||
setDecryptWith(keys, decrypt);
|
||||
|
||||
if (verifyOut != null && certs.isEmpty()) {
|
||||
String errorMessage = "Option %s is requested, but no option %s was provided.";
|
||||
throw new SOPGPException.IncompleteVerification(String.format(errorMessage, VERIFY_OUT, "--verify-with"));
|
||||
}
|
||||
|
||||
try {
|
||||
ReadyWithResult<DecryptionResult> ready = decrypt.ciphertext(System.in);
|
||||
DecryptionResult result = ready.writeTo(System.out);
|
||||
writeSessionKeyOut(result);
|
||||
writeVerifyOut(result);
|
||||
} catch (SOPGPException.BadData badData) {
|
||||
throw new SOPGPException.BadData("No valid OpenPGP message found on Standard Input.", badData);
|
||||
} catch (IOException ioException) {
|
||||
throw new RuntimeException(ioException);
|
||||
}
|
||||
}
|
||||
|
||||
private void throwIfOutputExists(File outputFile, String optionName) {
|
||||
if (outputFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (outputFile.exists()) {
|
||||
throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_OF_OPTION_EXISTS, outputFile.getAbsolutePath(), optionName));
|
||||
}
|
||||
}
|
||||
|
||||
private void writeVerifyOut(DecryptionResult result) throws IOException {
|
||||
if (verifyOut != null) {
|
||||
FileUtil.createNewFileOrThrow(verifyOut);
|
||||
try (FileOutputStream outputStream = new FileOutputStream(verifyOut)) {
|
||||
PrintWriter writer = new PrintWriter(outputStream);
|
||||
for (Verification verification : result.getVerifications()) {
|
||||
// CHECKSTYLE:OFF
|
||||
writer.println(verification.toString());
|
||||
// CHECKSTYLE:ON
|
||||
}
|
||||
writer.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeSessionKeyOut(DecryptionResult result) throws IOException {
|
||||
if (sessionKeyOut != null) {
|
||||
FileUtil.createNewFileOrThrow(sessionKeyOut);
|
||||
|
||||
try (FileOutputStream outputStream = new FileOutputStream(sessionKeyOut)) {
|
||||
if (!result.getSessionKey().isPresent()) {
|
||||
throw new SOPGPException.UnsupportedOption("Session key not extracted. Possibly the feature --session-key-out is not supported.");
|
||||
} else {
|
||||
SessionKey sessionKey = result.getSessionKey().get();
|
||||
outputStream.write(sessionKey.getAlgorithm());
|
||||
outputStream.write(sessionKey.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setDecryptWith(List<File> keys, Decrypt decrypt) {
|
||||
for (File key : keys) {
|
||||
try (FileInputStream keyIn = new FileInputStream(key)) {
|
||||
decrypt.withKey(keyIn);
|
||||
} catch (SOPGPException.KeyIsProtected keyIsProtected) {
|
||||
throw new SOPGPException.KeyIsProtected("Key in file " + key.getAbsolutePath() + " is password protected.", keyIsProtected);
|
||||
} catch (SOPGPException.BadData badData) {
|
||||
throw new SOPGPException.BadData("File " + key.getAbsolutePath() + " does not contain a private key.", badData);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, key.getAbsolutePath()), e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setVerifyWith(List<File> certs, Decrypt decrypt) {
|
||||
for (File cert : certs) {
|
||||
try (FileInputStream certIn = new FileInputStream(cert)) {
|
||||
decrypt.verifyWithCert(certIn);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, cert.getAbsolutePath()), e);
|
||||
} catch (SOPGPException.BadData badData) {
|
||||
throw new SOPGPException.BadData("File " + cert.getAbsolutePath() + " does not contain a valid certificate.", badData);
|
||||
} catch (IOException ioException) {
|
||||
throw new RuntimeException(ioException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setWithSessionKeys(List<String> withSessionKey, Decrypt decrypt) {
|
||||
Pattern sessionKeyPattern = Pattern.compile("^\\d+:[0-9A-F]+$");
|
||||
for (String sessionKey : withSessionKey) {
|
||||
if (!sessionKeyPattern.matcher(sessionKey).matches()) {
|
||||
throw new IllegalArgumentException("Session keys are expected in the format 'ALGONUM:HEXKEY'.");
|
||||
}
|
||||
String[] split = sessionKey.split(":");
|
||||
byte algorithm = (byte) Integer.parseInt(split[0]);
|
||||
byte[] key = HexUtil.hexToBytes(split[1]);
|
||||
|
||||
try {
|
||||
decrypt.withSessionKey(new SessionKey(algorithm, key));
|
||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-session-key"), unsupportedOption);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setWithPasswords(List<String> withPassword, Decrypt decrypt) {
|
||||
for (String password : withPassword) {
|
||||
try {
|
||||
decrypt.withPassword(password);
|
||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-password"), unsupportedOption);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setNotAfter(String notAfter, Decrypt decrypt) {
|
||||
Date notAfterDate = DateParser.parseNotAfter(notAfter);
|
||||
try {
|
||||
decrypt.verifyNotAfter(notAfterDate);
|
||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-after"), unsupportedOption);
|
||||
}
|
||||
}
|
||||
|
||||
private void setNotBefore(String notBefore, Decrypt decrypt) {
|
||||
Date notBeforeDate = DateParser.parseNotBefore(notBefore);
|
||||
try {
|
||||
decrypt.verifyNotBefore(notBeforeDate);
|
||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||
throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-before"), unsupportedOption);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.cli.picocli.commands;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import sop.Signatures;
|
||||
import sop.cli.picocli.SopCLI;
|
||||
import sop.exception.SOPGPException;
|
||||
import sop.operation.DetachInbandSignatureAndMessage;
|
||||
|
||||
@CommandLine.Command(name = "detach-inband-signature-and-message",
|
||||
description = "Split a clearsigned message",
|
||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
||||
public class DetachInbandSignatureAndMessageCmd implements Runnable {
|
||||
|
||||
@CommandLine.Option(
|
||||
names = {"--signatures-out"},
|
||||
description = "Destination to which a detached signatures block will be written",
|
||||
paramLabel = "SIGNATURES")
|
||||
File signaturesOut;
|
||||
|
||||
@CommandLine.Option(names = "--no-armor",
|
||||
description = "ASCII armor the output",
|
||||
negatable = true)
|
||||
boolean armor = true;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
DetachInbandSignatureAndMessage detach = SopCLI.getSop().detachInbandSignatureAndMessage();
|
||||
if (detach == null) {
|
||||
throw new SOPGPException.UnsupportedSubcommand("Command 'detach-inband-signature-and-message' not implemented.");
|
||||
}
|
||||
|
||||
if (signaturesOut == null) {
|
||||
throw new SOPGPException.MissingArg("--signatures-out is required.");
|
||||
}
|
||||
|
||||
if (!armor) {
|
||||
detach.noArmor();
|
||||
}
|
||||
|
||||
try {
|
||||
Signatures signatures = detach
|
||||
.message(System.in).writeTo(System.out);
|
||||
if (!signaturesOut.createNewFile()) {
|
||||
throw new SOPGPException.OutputExists("Destination of --signatures-out already exists.");
|
||||
}
|
||||
signatures.writeTo(new FileOutputStream(signaturesOut));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.cli.picocli.commands;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import sop.Ready;
|
||||
import sop.cli.picocli.SopCLI;
|
||||
import sop.enums.EncryptAs;
|
||||
import sop.exception.SOPGPException;
|
||||
import sop.operation.Encrypt;
|
||||
|
||||
@CommandLine.Command(name = "encrypt",
|
||||
description = "Encrypt a message from standard input",
|
||||
exitCodeOnInvalidInput = 37)
|
||||
public class EncryptCmd implements Runnable {
|
||||
|
||||
@CommandLine.Option(names = "--no-armor",
|
||||
description = "ASCII armor the output",
|
||||
negatable = true)
|
||||
boolean armor = true;
|
||||
|
||||
@CommandLine.Option(names = {"--as"},
|
||||
description = "Type of the input data. Defaults to 'binary'",
|
||||
paramLabel = "{binary|text|mime}")
|
||||
EncryptAs type;
|
||||
|
||||
@CommandLine.Option(names = "--with-password",
|
||||
description = "Encrypt the message with a password",
|
||||
paramLabel = "PASSWORD")
|
||||
List<String> withPassword = new ArrayList<>();
|
||||
|
||||
@CommandLine.Option(names = "--sign-with",
|
||||
description = "Sign the output with a private key",
|
||||
paramLabel = "KEY")
|
||||
List<File> signWith = new ArrayList<>();
|
||||
|
||||
@CommandLine.Parameters(description = "Certificates the message gets encrypted to",
|
||||
index = "0..*",
|
||||
paramLabel = "CERTS")
|
||||
List<File> certs = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Encrypt encrypt = SopCLI.getSop().encrypt();
|
||||
if (encrypt == null) {
|
||||
throw new SOPGPException.UnsupportedSubcommand("Command 'encrypt' not implemented.");
|
||||
}
|
||||
|
||||
if (type != null) {
|
||||
try {
|
||||
encrypt.mode(type);
|
||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||
throw new SOPGPException.UnsupportedOption("Unsupported option '--as'.", unsupportedOption);
|
||||
}
|
||||
}
|
||||
|
||||
if (withPassword.isEmpty() && certs.isEmpty()) {
|
||||
throw new SOPGPException.MissingArg("At least one password or cert file required for encryption.");
|
||||
}
|
||||
|
||||
for (String password : withPassword) {
|
||||
try {
|
||||
encrypt.withPassword(password);
|
||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||
throw new SOPGPException.UnsupportedOption("Unsupported option '--with-password'.", unsupportedOption);
|
||||
}
|
||||
}
|
||||
|
||||
for (File keyFile : signWith) {
|
||||
try (FileInputStream keyIn = new FileInputStream(keyFile)) {
|
||||
encrypt.signWith(keyIn);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new SOPGPException.MissingInput("Key file " + keyFile.getAbsolutePath() + " not found.", e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (SOPGPException.KeyIsProtected keyIsProtected) {
|
||||
throw new SOPGPException.KeyIsProtected("Key from " + keyFile.getAbsolutePath() + " is password protected.", keyIsProtected);
|
||||
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
|
||||
throw new SOPGPException.UnsupportedAsymmetricAlgo("Key from " + keyFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo);
|
||||
} catch (SOPGPException.KeyCannotSign keyCannotSign) {
|
||||
throw new SOPGPException.KeyCannotSign("Key from " + keyFile.getAbsolutePath() + " cannot sign.", keyCannotSign);
|
||||
} catch (SOPGPException.BadData badData) {
|
||||
throw new SOPGPException.BadData("Key file " + keyFile.getAbsolutePath() + " does not contain a valid OpenPGP private key.", badData);
|
||||
}
|
||||
}
|
||||
|
||||
for (File certFile : certs) {
|
||||
try (FileInputStream certIn = new FileInputStream(certFile)) {
|
||||
encrypt.withCert(certIn);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new SOPGPException.MissingInput("Certificate file " + certFile.getAbsolutePath() + " not found.", e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
|
||||
throw new SOPGPException.UnsupportedAsymmetricAlgo("Certificate from " + certFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo);
|
||||
} catch (SOPGPException.CertCannotEncrypt certCannotEncrypt) {
|
||||
throw new SOPGPException.CertCannotEncrypt("Certificate from " + certFile.getAbsolutePath() + " is not capable of encryption.", certCannotEncrypt);
|
||||
} catch (SOPGPException.BadData badData) {
|
||||
throw new SOPGPException.BadData("Certificate file " + certFile.getAbsolutePath() + " does not contain a valid OpenPGP certificate.", badData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!armor) {
|
||||
encrypt.noArmor();
|
||||
}
|
||||
|
||||
try {
|
||||
Ready ready = encrypt.plaintext(System.in);
|
||||
ready.writeTo(System.out);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,45 +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",
|
||||
description = "Extract a public key certificate from a secret key from standard input",
|
||||
exitCodeOnInvalidInput = 37)
|
||||
public class ExtractCertCmd implements Runnable {
|
||||
|
||||
@CommandLine.Option(names = "--no-armor",
|
||||
description = "ASCII armor the output",
|
||||
negatable = true)
|
||||
boolean armor = true;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
ExtractCert extractCert = SopCLI.getSop().extractCert();
|
||||
if (extractCert == null) {
|
||||
throw new SOPGPException.UnsupportedSubcommand("Command 'extract-cert' not implemented.");
|
||||
}
|
||||
|
||||
if (!armor) {
|
||||
extractCert.noArmor();
|
||||
}
|
||||
|
||||
try {
|
||||
Ready ready = extractCert.key(System.in);
|
||||
ready.writeTo(System.out);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (SOPGPException.BadData badData) {
|
||||
throw new SOPGPException.BadData("Standard Input does not contain valid OpenPGP private key material.", badData);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,63 +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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import sop.Ready;
|
||||
import sop.cli.picocli.Print;
|
||||
import sop.cli.picocli.SopCLI;
|
||||
import sop.exception.SOPGPException;
|
||||
import sop.operation.GenerateKey;
|
||||
|
||||
@CommandLine.Command(name = "generate-key",
|
||||
description = "Generate a secret key",
|
||||
exitCodeOnInvalidInput = 37)
|
||||
public class GenerateKeyCmd implements Runnable {
|
||||
|
||||
@CommandLine.Option(names = "--no-armor",
|
||||
description = "ASCII armor the output",
|
||||
negatable = true)
|
||||
boolean armor = true;
|
||||
|
||||
@CommandLine.Parameters(description = "User-ID, eg. \"Alice <alice@example.com>\"")
|
||||
List<String> userId = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
GenerateKey generateKey = SopCLI.getSop().generateKey();
|
||||
if (generateKey == null) {
|
||||
throw new SOPGPException.UnsupportedSubcommand("Command 'generate-key' not implemented.");
|
||||
}
|
||||
|
||||
for (String userId : userId) {
|
||||
generateKey.userId(userId);
|
||||
}
|
||||
|
||||
if (!armor) {
|
||||
generateKey.noArmor();
|
||||
}
|
||||
|
||||
try {
|
||||
Ready ready = generateKey.generate();
|
||||
ready.writeTo(System.out);
|
||||
} catch (SOPGPException.MissingArg missingArg) {
|
||||
Print.errln("Missing argument.");
|
||||
Print.trace(missingArg);
|
||||
System.exit(missingArg.getExitCode());
|
||||
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
|
||||
Print.errln("Unsupported asymmetric algorithm.");
|
||||
Print.trace(unsupportedAsymmetricAlgo);
|
||||
System.exit(unsupportedAsymmetricAlgo.getExitCode());
|
||||
} catch (IOException e) {
|
||||
Print.errln("IO Error.");
|
||||
Print.trace(e);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.cli.picocli.commands;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import sop.MicAlg;
|
||||
import sop.ReadyWithResult;
|
||||
import sop.SigningResult;
|
||||
import sop.cli.picocli.Print;
|
||||
import sop.cli.picocli.SopCLI;
|
||||
import sop.enums.SignAs;
|
||||
import sop.exception.SOPGPException;
|
||||
import sop.operation.Sign;
|
||||
|
||||
@CommandLine.Command(name = "sign",
|
||||
description = "Create a detached signature on the data from standard input",
|
||||
exitCodeOnInvalidInput = 37)
|
||||
public class SignCmd implements Runnable {
|
||||
|
||||
@CommandLine.Option(names = "--no-armor",
|
||||
description = "ASCII armor the output",
|
||||
negatable = true)
|
||||
boolean armor = true;
|
||||
|
||||
@CommandLine.Option(names = "--as", description = "Defaults to 'binary'. If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53.",
|
||||
paramLabel = "{binary|text}")
|
||||
SignAs type;
|
||||
|
||||
@CommandLine.Parameters(description = "Secret keys used for signing",
|
||||
paramLabel = "KEYS")
|
||||
List<File> secretKeyFile = new ArrayList<>();
|
||||
|
||||
@CommandLine.Option(names = "--micalg-out", description = "Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156)",
|
||||
paramLabel = "MICALG")
|
||||
File micAlgOut;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Sign sign = SopCLI.getSop().sign();
|
||||
if (sign == null) {
|
||||
throw new SOPGPException.UnsupportedSubcommand("Command 'sign' not implemented.");
|
||||
}
|
||||
|
||||
if (type != null) {
|
||||
try {
|
||||
sign.mode(type);
|
||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||
Print.errln("Unsupported option '--as'");
|
||||
Print.trace(unsupportedOption);
|
||||
System.exit(unsupportedOption.getExitCode());
|
||||
}
|
||||
}
|
||||
|
||||
if (micAlgOut != null && micAlgOut.exists()) {
|
||||
throw new SOPGPException.OutputExists(String.format("Target %s of option %s already exists.", micAlgOut.getAbsolutePath(), "--micalg-out"));
|
||||
}
|
||||
|
||||
if (secretKeyFile.isEmpty()) {
|
||||
Print.errln("Missing required parameter 'KEYS'.");
|
||||
System.exit(19);
|
||||
}
|
||||
|
||||
for (File keyFile : secretKeyFile) {
|
||||
try (FileInputStream keyIn = new FileInputStream(keyFile)) {
|
||||
sign.key(keyIn);
|
||||
} catch (FileNotFoundException e) {
|
||||
Print.errln("File " + keyFile.getAbsolutePath() + " does not exist.");
|
||||
Print.trace(e);
|
||||
System.exit(1);
|
||||
} catch (IOException e) {
|
||||
Print.errln("Cannot access file " + keyFile.getAbsolutePath());
|
||||
Print.trace(e);
|
||||
System.exit(1);
|
||||
} catch (SOPGPException.KeyIsProtected e) {
|
||||
Print.errln("Key " + keyFile.getName() + " is password protected.");
|
||||
Print.trace(e);
|
||||
System.exit(1);
|
||||
} catch (SOPGPException.BadData badData) {
|
||||
Print.errln("Bad data in key file " + keyFile.getAbsolutePath() + ":");
|
||||
Print.trace(badData);
|
||||
System.exit(badData.getExitCode());
|
||||
}
|
||||
}
|
||||
|
||||
if (!armor) {
|
||||
sign.noArmor();
|
||||
}
|
||||
|
||||
try {
|
||||
ReadyWithResult<SigningResult> ready = sign.data(System.in);
|
||||
SigningResult result = ready.writeTo(System.out);
|
||||
|
||||
MicAlg micAlg = result.getMicAlg();
|
||||
if (micAlgOut != null) {
|
||||
// Write micalg out
|
||||
micAlgOut.createNewFile();
|
||||
FileOutputStream micAlgOutStream = new FileOutputStream(micAlgOut);
|
||||
micAlg.writeTo(micAlgOutStream);
|
||||
micAlgOutStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Print.errln("IO Error.");
|
||||
Print.trace(e);
|
||||
System.exit(1);
|
||||
} catch (SOPGPException.ExpectedText expectedText) {
|
||||
Print.errln("Expected text input, but got binary data.");
|
||||
Print.trace(expectedText);
|
||||
System.exit(expectedText.getExitCode());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.cli.picocli.commands;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import sop.Verification;
|
||||
import sop.cli.picocli.DateParser;
|
||||
import sop.cli.picocli.Print;
|
||||
import sop.cli.picocli.SopCLI;
|
||||
import sop.exception.SOPGPException;
|
||||
import sop.operation.Verify;
|
||||
|
||||
@CommandLine.Command(name = "verify",
|
||||
description = "Verify a detached signature over the data from standard input",
|
||||
exitCodeOnInvalidInput = 37)
|
||||
public class VerifyCmd implements Runnable {
|
||||
|
||||
@CommandLine.Parameters(index = "0",
|
||||
description = "Detached signature",
|
||||
paramLabel = "SIGNATURE")
|
||||
File signature;
|
||||
|
||||
@CommandLine.Parameters(index = "1..*",
|
||||
arity = "1..*",
|
||||
description = "Public key certificates",
|
||||
paramLabel = "CERT")
|
||||
List<File> certificates = new ArrayList<>();
|
||||
|
||||
@CommandLine.Option(names = {"--not-before"},
|
||||
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
|
||||
"Reject signatures with a creation date not in range.\n" +
|
||||
"Defaults to beginning of time (\"-\").",
|
||||
paramLabel = "DATE")
|
||||
String notBefore = "-";
|
||||
|
||||
@CommandLine.Option(names = {"--not-after"},
|
||||
description = "ISO-8601 formatted UTC date (eg. '2020-11-23T16:35Z)\n" +
|
||||
"Reject signatures with a creation date not in range.\n" +
|
||||
"Defaults to current system time (\"now\").\n" +
|
||||
"Accepts special value \"-\" for end of time.",
|
||||
paramLabel = "DATE")
|
||||
String notAfter = "now";
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Verify verify = SopCLI.getSop().verify();
|
||||
if (verify == null) {
|
||||
throw new SOPGPException.UnsupportedSubcommand("Command 'verify' not implemented.");
|
||||
}
|
||||
|
||||
if (notAfter != null) {
|
||||
try {
|
||||
verify.notAfter(DateParser.parseNotAfter(notAfter));
|
||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||
Print.errln("Unsupported option '--not-after'.");
|
||||
Print.trace(unsupportedOption);
|
||||
System.exit(unsupportedOption.getExitCode());
|
||||
}
|
||||
}
|
||||
if (notBefore != null) {
|
||||
try {
|
||||
verify.notBefore(DateParser.parseNotBefore(notBefore));
|
||||
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
|
||||
Print.errln("Unsupported option '--not-before'.");
|
||||
Print.trace(unsupportedOption);
|
||||
System.exit(unsupportedOption.getExitCode());
|
||||
}
|
||||
}
|
||||
|
||||
for (File certFile : certificates) {
|
||||
try (FileInputStream certIn = new FileInputStream(certFile)) {
|
||||
verify.cert(certIn);
|
||||
} catch (FileNotFoundException fileNotFoundException) {
|
||||
Print.errln("Certificate file " + certFile.getAbsolutePath() + " not found.");
|
||||
|
||||
Print.trace(fileNotFoundException);
|
||||
System.exit(1);
|
||||
} catch (IOException ioException) {
|
||||
Print.errln("IO Error.");
|
||||
Print.trace(ioException);
|
||||
System.exit(1);
|
||||
} catch (SOPGPException.BadData badData) {
|
||||
Print.errln("Certificate file " + certFile.getAbsolutePath() + " appears to not contain a valid OpenPGP certificate.");
|
||||
Print.trace(badData);
|
||||
System.exit(badData.getExitCode());
|
||||
}
|
||||
}
|
||||
|
||||
if (signature != null) {
|
||||
try (FileInputStream sigIn = new FileInputStream(signature)) {
|
||||
verify.signatures(sigIn);
|
||||
} catch (FileNotFoundException e) {
|
||||
Print.errln("Signature file " + signature.getAbsolutePath() + " does not exist.");
|
||||
Print.trace(e);
|
||||
System.exit(1);
|
||||
} catch (IOException e) {
|
||||
Print.errln("IO Error.");
|
||||
Print.trace(e);
|
||||
System.exit(1);
|
||||
} catch (SOPGPException.BadData badData) {
|
||||
Print.errln("File " + signature.getAbsolutePath() + " does not contain a valid OpenPGP signature.");
|
||||
Print.trace(badData);
|
||||
System.exit(badData.getExitCode());
|
||||
}
|
||||
}
|
||||
|
||||
List<Verification> verifications = null;
|
||||
try {
|
||||
verifications = verify.data(System.in);
|
||||
} catch (SOPGPException.NoSignature e) {
|
||||
Print.errln("No verifiable signature found.");
|
||||
Print.trace(e);
|
||||
System.exit(e.getExitCode());
|
||||
} catch (IOException ioException) {
|
||||
Print.errln("IO Error.");
|
||||
Print.trace(ioException);
|
||||
System.exit(1);
|
||||
} catch (SOPGPException.BadData badData) {
|
||||
Print.errln("Standard Input appears not to contain a valid OpenPGP message.");
|
||||
Print.trace(badData);
|
||||
System.exit(badData.getExitCode());
|
||||
}
|
||||
for (Verification verification : verifications) {
|
||||
Print.outln(verification.toString());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,52 +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", description = "Display version information about the tool",
|
||||
exitCodeOnInvalidInput = 37)
|
||||
public class VersionCmd implements Runnable {
|
||||
|
||||
@CommandLine.ArgGroup()
|
||||
Exclusive exclusive;
|
||||
|
||||
static class Exclusive {
|
||||
@CommandLine.Option(names = "--extended", description = "Print an extended version string.")
|
||||
boolean extended;
|
||||
|
||||
@CommandLine.Option(names = "--backend", description = "Print information about the cryptographic backend.")
|
||||
boolean backend;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Version version = SopCLI.getSop().version();
|
||||
if (version == null) {
|
||||
throw new SOPGPException.UnsupportedSubcommand("Command 'version' not implemented.");
|
||||
}
|
||||
|
||||
if (exclusive == null) {
|
||||
Print.outln(version.getName() + " " + version.getVersion());
|
||||
return;
|
||||
}
|
||||
|
||||
if (exclusive.extended) {
|
||||
Print.outln(version.getExtendedVersion());
|
||||
return;
|
||||
}
|
||||
|
||||
if (exclusive.backend) {
|
||||
Print.outln(version.getBackendVersion());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
121
sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt
Normal file
121
sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt
Normal file
|
@ -0,0 +1,121 @@
|
|||
// 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,
|
||||
UpdateKeyCmd::class,
|
||||
MergeCertsCmd::class,
|
||||
CertifyUserIdCmd::class,
|
||||
ValidateUserIdCmd::class,
|
||||
// Messaging subcommands
|
||||
SignCmd::class,
|
||||
VerifyCmd::class,
|
||||
EncryptCmd::class,
|
||||
DecryptCmd::class,
|
||||
InlineDetachCmd::class,
|
||||
InlineSignCmd::class,
|
||||
InlineVerifyCmd::class,
|
||||
// Transport
|
||||
ArmorCmd::class,
|
||||
DearmorCmd::class,
|
||||
// misc
|
||||
HelpCommand::class,
|
||||
GenerateCompletion::class])
|
||||
class SopCLI {
|
||||
|
||||
companion object {
|
||||
@JvmStatic private var sopInstance: SOP? = null
|
||||
|
||||
@JvmStatic
|
||||
fun getSop(): SOP =
|
||||
checkNotNull(sopInstance) { cliMsg.getString("sop.error.runtime.no_backend_set") }
|
||||
|
||||
@JvmStatic
|
||||
fun setSopInstance(sop: SOP?) {
|
||||
sopInstance = sop
|
||||
}
|
||||
|
||||
@JvmField var cliMsg: ResourceBundle = ResourceBundle.getBundle("msg_sop")
|
||||
|
||||
@JvmField var EXECUTABLE_NAME = "sop"
|
||||
|
||||
@JvmField
|
||||
@Option(names = ["--stacktrace", "--debug"], scope = ScopeType.INHERIT)
|
||||
var stacktrace = false
|
||||
|
||||
@JvmStatic
|
||||
fun main(vararg args: String) {
|
||||
val exitCode = execute(*args)
|
||||
if (exitCode != 0) {
|
||||
exitProcess(exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun execute(vararg args: String): Int {
|
||||
// Set locale
|
||||
CommandLine(InitLocale()).setUnmatchedArgumentsAllowed(true).parseArgs(*args)
|
||||
|
||||
// Re-set bundle with updated locale
|
||||
cliMsg = ResourceBundle.getBundle("msg_sop")
|
||||
|
||||
return CommandLine(SopCLI::class.java)
|
||||
.apply {
|
||||
// Hide generate-completion command
|
||||
subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true)
|
||||
// render Input/Output sections in help command
|
||||
subcommands.values
|
||||
.filter {
|
||||
(it.getCommand() as Any) is AbstractSopCmd
|
||||
} // Only for AbstractSopCmd objects
|
||||
.forEach { (it.getCommand() as AbstractSopCmd).installIORenderer(it) }
|
||||
// overwrite executable name
|
||||
commandName = EXECUTABLE_NAME
|
||||
// setup exception handling
|
||||
executionExceptionHandler = SOPExecutionExceptionHandler()
|
||||
exitCodeExceptionMapper = SOPExceptionExitCodeMapper()
|
||||
isCaseInsensitiveEnumValuesAllowed = true
|
||||
}
|
||||
.execute(*args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Control the locale.
|
||||
*
|
||||
* @see <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
|
||||
}
|
||||
}
|
99
sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt
Normal file
99
sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt
Normal file
|
@ -0,0 +1,99 @@
|
|||
// 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", "--debug"], scope = CommandLine.ScopeType.INHERIT)
|
||||
var stacktrace = false
|
||||
|
||||
@JvmStatic
|
||||
fun main(vararg args: String) {
|
||||
val exitCode = execute(*args)
|
||||
if (exitCode != 0) {
|
||||
exitProcess(exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun execute(vararg args: String): Int {
|
||||
// Set locale
|
||||
CommandLine(InitLocale()).parseArgs(*args)
|
||||
|
||||
// Re-set bundle with updated locale
|
||||
cliMsg = ResourceBundle.getBundle("msg_sop")
|
||||
|
||||
return CommandLine(SopVCLI::class.java)
|
||||
.apply {
|
||||
// explicitly set help command resource bundle
|
||||
subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help"))
|
||||
// Hide generate-completion command
|
||||
subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true)
|
||||
// overwrite executable name
|
||||
commandName = EXECUTABLE_NAME
|
||||
// setup exception handling
|
||||
executionExceptionHandler = SOPExecutionExceptionHandler()
|
||||
exitCodeExceptionMapper = SOPExceptionExitCodeMapper()
|
||||
isCaseInsensitiveEnumValuesAllowed = true
|
||||
}
|
||||
.execute(*args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Control the locale.
|
||||
*
|
||||
* @see <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,348 @@
|
|||
// 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 picocli.CommandLine
|
||||
import picocli.CommandLine.Help
|
||||
import picocli.CommandLine.Help.Column
|
||||
import picocli.CommandLine.Help.TextTable
|
||||
import picocli.CommandLine.IHelpSectionRenderer
|
||||
import sop.cli.picocli.commands.AbstractSopCmd.EnvironmentVariableResolver
|
||||
import sop.exception.SOPGPException.*
|
||||
import sop.util.UTCUtil.Companion.parseUTCDate
|
||||
import sop.util.UTF8Util.Companion.decodeUTF8
|
||||
|
||||
/** Abstract super class of SOP subcommands. */
|
||||
abstract class AbstractSopCmd(locale: Locale = Locale.getDefault()) : Runnable {
|
||||
|
||||
private val messages: ResourceBundle = ResourceBundle.getBundle("msg_sop", locale)
|
||||
var environmentVariableResolver = EnvironmentVariableResolver { name: String ->
|
||||
System.getenv(name)
|
||||
}
|
||||
|
||||
/** Interface to modularize resolving of environment variables. */
|
||||
fun interface EnvironmentVariableResolver {
|
||||
|
||||
/**
|
||||
* Resolve the value of the given environment variable. Return null if the variable is not
|
||||
* present.
|
||||
*
|
||||
* @param name name of the variable
|
||||
* @return variable value or null
|
||||
*/
|
||||
fun resolveEnvironmentVariable(name: String): String?
|
||||
}
|
||||
|
||||
fun throwIfOutputExists(output: String?) {
|
||||
output
|
||||
?.let { File(it) }
|
||||
?.let {
|
||||
if (it.exists()) {
|
||||
val errorMsg: String =
|
||||
getMsg(
|
||||
"sop.error.indirect_data_type.output_file_already_exists",
|
||||
it.absolutePath)
|
||||
throw OutputExists(errorMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getMsg(key: String): String = messages.getString(key)
|
||||
|
||||
fun getMsg(key: String, vararg args: String): String {
|
||||
val msg = messages.getString(key)
|
||||
return String.format(msg, *args)
|
||||
}
|
||||
|
||||
fun throwIfMissingArg(arg: Any?, argName: String) {
|
||||
if (arg == null) {
|
||||
val errorMsg = getMsg("sop.error.usage.argument_required", argName)
|
||||
throw MissingArg(errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
fun throwIfEmptyParameters(arg: Collection<*>, parmName: String) {
|
||||
if (arg.isEmpty()) {
|
||||
val errorMsg = getMsg("sop.error.usage.parameter_required", parmName)
|
||||
throw MissingArg(errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
fun <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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See
|
||||
* [Example](https://github.com/remkop/picocli/blob/main/picocli-examples/src/main/java/picocli/examples/customhelp/EnvironmentVariablesSection.java)
|
||||
*/
|
||||
class InputOutputHelpSectionRenderer(private val argument: Pair<String?, String?>) :
|
||||
IHelpSectionRenderer {
|
||||
|
||||
override fun render(help: Help): String {
|
||||
return argument.let {
|
||||
val calcLen =
|
||||
help.calcLongOptionColumnWidth(
|
||||
help.commandSpec().options(),
|
||||
help.commandSpec().positionalParameters(),
|
||||
help.colorScheme())
|
||||
val keyLength =
|
||||
help
|
||||
.commandSpec()
|
||||
.usageMessage()
|
||||
.longOptionsMaxWidth()
|
||||
.coerceAtMost(calcLen - 1)
|
||||
val table =
|
||||
TextTable.forColumns(
|
||||
help.colorScheme(),
|
||||
Column(keyLength + 7, 6, Column.Overflow.SPAN),
|
||||
Column(width(help) - (keyLength + 7), 0, Column.Overflow.WRAP))
|
||||
table.setAdjustLineBreaksForWideCJKCharacters(adjustCJK(help))
|
||||
table.addRowValues("@|yellow ${argument.first}|@", argument.second ?: "")
|
||||
table.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun adjustCJK(help: Help) =
|
||||
help.commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters()
|
||||
|
||||
private fun width(help: Help) = help.commandSpec().usageMessage().width()
|
||||
}
|
||||
|
||||
fun installIORenderer(cmd: CommandLine) {
|
||||
val inputName = getResString(cmd, "standardInput")
|
||||
if (inputName != null) {
|
||||
cmd.helpSectionMap[SECTION_KEY_STANDARD_INPUT_HEADING] = IHelpSectionRenderer {
|
||||
getResString(cmd, "standardInputHeading")
|
||||
}
|
||||
cmd.helpSectionMap[SECTION_KEY_STANDARD_INPUT_DETAILS] =
|
||||
InputOutputHelpSectionRenderer(
|
||||
inputName to getResString(cmd, "standardInputDescription"))
|
||||
cmd.helpSectionKeys =
|
||||
insertKey(
|
||||
cmd.helpSectionKeys,
|
||||
SECTION_KEY_STANDARD_INPUT_HEADING,
|
||||
SECTION_KEY_STANDARD_INPUT_DETAILS)
|
||||
}
|
||||
|
||||
val outputName = getResString(cmd, "standardOutput")
|
||||
if (outputName != null) {
|
||||
cmd.helpSectionMap[SECTION_KEY_STANDARD_OUTPUT_HEADING] = IHelpSectionRenderer {
|
||||
getResString(cmd, "standardOutputHeading")
|
||||
}
|
||||
cmd.helpSectionMap[SECTION_KEY_STANDARD_OUTPUT_DETAILS] =
|
||||
InputOutputHelpSectionRenderer(
|
||||
outputName to getResString(cmd, "standardOutputDescription"))
|
||||
cmd.helpSectionKeys =
|
||||
insertKey(
|
||||
cmd.helpSectionKeys,
|
||||
SECTION_KEY_STANDARD_OUTPUT_HEADING,
|
||||
SECTION_KEY_STANDARD_OUTPUT_DETAILS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertKey(keys: List<String>, header: String, details: String): List<String> {
|
||||
val index =
|
||||
keys.indexOf(CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST_HEADING)
|
||||
val result = keys.toMutableList()
|
||||
result.add(index, header)
|
||||
result.add(index + 1, details)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getResString(cmd: CommandLine, key: String): String? =
|
||||
try {
|
||||
cmd.resourceBundle.getString(key)
|
||||
} catch (m: MissingResourceException) {
|
||||
try {
|
||||
cmd.parent.resourceBundle.getString(key)
|
||||
} catch (m: MissingResourceException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
?.let { String.format(it) }
|
||||
|
||||
companion object {
|
||||
const val PRFX_ENV = "@ENV:"
|
||||
|
||||
const val PRFX_FD = "@FD:"
|
||||
|
||||
const val SECTION_KEY_STANDARD_INPUT_HEADING = "standardInputHeading"
|
||||
const val SECTION_KEY_STANDARD_INPUT_DETAILS = "standardInput"
|
||||
const val SECTION_KEY_STANDARD_OUTPUT_HEADING = "standardOutputHeading"
|
||||
const val SECTION_KEY_STANDARD_OUTPUT_DETAILS = "standardOutput"
|
||||
|
||||
@JvmField val DAWN_OF_TIME = Date(0)
|
||||
|
||||
@JvmField
|
||||
@Deprecated("Replace with DAWN_OF_TIME", ReplaceWith("DAWN_OF_TIME"))
|
||||
val BEGINNING_OF_TIME = DAWN_OF_TIME
|
||||
|
||||
@JvmField val END_OF_TIME = Date(8640000000000000L)
|
||||
|
||||
@JvmField val PATTERN_FD = "^\\d{1,20}$".toPattern()
|
||||
|
||||
@Throws(IOException::class)
|
||||
@JvmStatic
|
||||
fun stringFromInputStream(inputStream: InputStream): String {
|
||||
return inputStream.use { input ->
|
||||
val byteOut = ByteArrayOutputStream()
|
||||
val buf = ByteArray(4096)
|
||||
var read: Int
|
||||
while (input.read(buf).also { read = it } != -1) {
|
||||
byteOut.write(buf, 0, read)
|
||||
}
|
||||
// TODO: For decrypt operations we MUST accept non-UTF8 passwords
|
||||
decodeUTF8(byteOut.toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,84 @@
|
|||
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.cli.picocli.commands
|
||||
|
||||
import java.io.IOException
|
||||
import picocli.CommandLine.Command
|
||||
import picocli.CommandLine.Option
|
||||
import picocli.CommandLine.Parameters
|
||||
import sop.cli.picocli.SopCLI
|
||||
import sop.exception.SOPGPException.BadData
|
||||
import sop.exception.SOPGPException.UnsupportedOption
|
||||
|
||||
@Command(
|
||||
name = "certify-userid",
|
||||
resourceBundle = "msg_certify-userid",
|
||||
exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE,
|
||||
showEndOfOptionsDelimiterInUsageHelp = true)
|
||||
class CertifyUserIdCmd : AbstractSopCmd() {
|
||||
|
||||
@Option(names = ["--no-armor"], negatable = true) var armor = true
|
||||
|
||||
@Option(names = ["--userid"], required = true, arity = "1..*", paramLabel = "USERID")
|
||||
var userIds: List<String> = listOf()
|
||||
|
||||
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
|
||||
var withKeyPassword: List<String> = listOf()
|
||||
|
||||
@Option(names = ["--no-require-self-sig"]) var noRequireSelfSig = false
|
||||
|
||||
@Parameters(paramLabel = "KEYS", arity = "1..*") var keys: List<String> = listOf()
|
||||
|
||||
override fun run() {
|
||||
val certifyUserId =
|
||||
throwIfUnsupportedSubcommand(SopCLI.getSop().certifyUserId(), "certify-userid")
|
||||
|
||||
if (!armor) {
|
||||
certifyUserId.noArmor()
|
||||
}
|
||||
|
||||
if (noRequireSelfSig) {
|
||||
certifyUserId.noRequireSelfSig()
|
||||
}
|
||||
|
||||
for (userId in userIds) {
|
||||
certifyUserId.userId(userId)
|
||||
}
|
||||
|
||||
for (passwordFileName in withKeyPassword) {
|
||||
try {
|
||||
val password = stringFromInputStream(getInput(passwordFileName))
|
||||
certifyUserId.withKeyPassword(password)
|
||||
} catch (unsupportedOption: UnsupportedOption) {
|
||||
val errorMsg =
|
||||
getMsg("sop.error.feature_support.option_not_supported", "--with-key-password")
|
||||
throw UnsupportedOption(errorMsg, unsupportedOption)
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
for (keyInput in keys) {
|
||||
try {
|
||||
getInput(keyInput).use { certifyUserId.keys(it) }
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
} catch (badData: BadData) {
|
||||
val errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput)
|
||||
throw BadData(errorMsg, badData)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val ready = certifyUserId.certs(System.`in`)
|
||||
ready.writeTo(System.out)
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
} catch (badData: BadData) {
|
||||
val errorMsg = getMsg("sop.error.input.not_a_private_key", "STDIN")
|
||||
throw BadData(errorMsg, badData)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
// 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 {
|
||||
val password = stringFromInputStream(getInput(it))
|
||||
changeKeyPassword.oldKeyPassphrase(password)
|
||||
}
|
||||
|
||||
newKeyPassword?.let {
|
||||
val password = stringFromInputStream(getInput(it))
|
||||
changeKeyPassword.newKeyPassphrase(password)
|
||||
}
|
||||
|
||||
try {
|
||||
changeKeyPassword.keys(System.`in`).writeTo(System.out)
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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, "--verify-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,46 @@
|
|||
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.cli.picocli.commands
|
||||
|
||||
import java.io.IOException
|
||||
import picocli.CommandLine
|
||||
import picocli.CommandLine.Command
|
||||
import sop.cli.picocli.SopCLI
|
||||
import sop.exception.SOPGPException
|
||||
|
||||
@Command(
|
||||
name = "merge-certs",
|
||||
resourceBundle = "msg_merge-certs",
|
||||
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
|
||||
class MergeCertsCmd : AbstractSopCmd() {
|
||||
|
||||
@CommandLine.Option(names = ["--no-armor"], negatable = true) var armor = true
|
||||
|
||||
@CommandLine.Parameters(paramLabel = "CERTS") var updates: List<String> = listOf()
|
||||
|
||||
override fun run() {
|
||||
val mergeCerts = throwIfUnsupportedSubcommand(SopCLI.getSop().mergeCerts(), "merge-certs")
|
||||
|
||||
if (!armor) {
|
||||
mergeCerts.noArmor()
|
||||
}
|
||||
|
||||
for (certFileName in updates) {
|
||||
try {
|
||||
getInput(certFileName).use { mergeCerts.updates(it) }
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
val ready = mergeCerts.baseCertificates(System.`in`)
|
||||
ready.writeTo(System.out)
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,79 @@
|
|||
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.cli.picocli.commands
|
||||
|
||||
import java.io.IOException
|
||||
import picocli.CommandLine.Command
|
||||
import picocli.CommandLine.Option
|
||||
import sop.cli.picocli.SopCLI
|
||||
import sop.exception.SOPGPException.*
|
||||
|
||||
@Command(
|
||||
name = "update-key",
|
||||
resourceBundle = "msg_update-key",
|
||||
exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE)
|
||||
class UpdateKeyCmd : AbstractSopCmd() {
|
||||
|
||||
@Option(names = ["--no-armor"], negatable = true) var armor = true
|
||||
|
||||
@Option(names = ["--signing-only"]) var signingOnly = false
|
||||
|
||||
@Option(names = ["--no-added-capabilities"]) var noAddedCapabilities = false
|
||||
|
||||
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
|
||||
var withKeyPassword: List<String> = listOf()
|
||||
|
||||
@Option(names = ["--merge-certs"], paramLabel = "CERTS") var mergeCerts: List<String> = listOf()
|
||||
|
||||
override fun run() {
|
||||
val updateKey = throwIfUnsupportedSubcommand(SopCLI.getSop().updateKey(), "update-key")
|
||||
|
||||
if (!armor) {
|
||||
updateKey.noArmor()
|
||||
}
|
||||
|
||||
if (signingOnly) {
|
||||
updateKey.signingOnly()
|
||||
}
|
||||
|
||||
if (noAddedCapabilities) {
|
||||
updateKey.noAddedCapabilities()
|
||||
}
|
||||
|
||||
for (passwordFileName in withKeyPassword) {
|
||||
try {
|
||||
val password = stringFromInputStream(getInput(passwordFileName))
|
||||
updateKey.withKeyPassword(password)
|
||||
} catch (unsupportedOption: UnsupportedOption) {
|
||||
val errorMsg =
|
||||
getMsg("sop.error.feature_support.option_not_supported", "--with-key-password")
|
||||
throw UnsupportedOption(errorMsg, unsupportedOption)
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
for (certInput in mergeCerts) {
|
||||
try {
|
||||
getInput(certInput).use { updateKey.mergeCerts(it) }
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
} catch (badData: BadData) {
|
||||
val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput)
|
||||
throw BadData(errorMsg, badData)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val ready = updateKey.key(System.`in`)
|
||||
ready.writeTo(System.out)
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
} catch (badData: BadData) {
|
||||
val errorMsg = getMsg("sop.error.input.not_a_private_key", "STDIN")
|
||||
throw BadData(errorMsg, badData)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sop.cli.picocli.commands
|
||||
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import picocli.CommandLine.Command
|
||||
import picocli.CommandLine.Option
|
||||
import picocli.CommandLine.Parameters
|
||||
import sop.cli.picocli.SopCLI
|
||||
import sop.exception.SOPGPException
|
||||
import sop.util.HexUtil.Companion.bytesToHex
|
||||
|
||||
@Command(
|
||||
name = "validate-userid",
|
||||
resourceBundle = "msg_validate-userid",
|
||||
exitCodeOnInvalidInput = SOPGPException.MissingArg.EXIT_CODE,
|
||||
showEndOfOptionsDelimiterInUsageHelp = true)
|
||||
class ValidateUserIdCmd : AbstractSopCmd() {
|
||||
|
||||
@Option(names = ["--addr-spec-only"]) var addrSpecOnly: Boolean = false
|
||||
|
||||
@Option(names = ["--validate-at"]) var validateAt: Date? = null
|
||||
|
||||
@Parameters(index = "0", arity = "1", paramLabel = "USERID") lateinit var userId: String
|
||||
|
||||
@Parameters(index = "1..*", arity = "1..*", paramLabel = "CERTS")
|
||||
var authorities: List<String> = listOf()
|
||||
|
||||
override fun run() {
|
||||
val validateUserId =
|
||||
throwIfUnsupportedSubcommand(SopCLI.getSop().validateUserId(), "validate-userid")
|
||||
|
||||
if (addrSpecOnly) {
|
||||
validateUserId.addrSpecOnly()
|
||||
}
|
||||
|
||||
if (validateAt != null) {
|
||||
validateUserId.validateAt(validateAt!!)
|
||||
}
|
||||
|
||||
validateUserId.userId(userId)
|
||||
|
||||
for (authority in authorities) {
|
||||
try {
|
||||
getInput(authority).use { validateUserId.authorities(it) }
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
} catch (b: SOPGPException.BadData) {
|
||||
val errorMsg = getMsg("sop.error.input.not_a_certificate", authority)
|
||||
throw SOPGPException.BadData(errorMsg, b)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val valid = validateUserId.subjects(System.`in`)
|
||||
|
||||
if (!valid) {
|
||||
val errorMsg = getMsg("sop.error.runtime.any_cert_user_id_no_match", userId)
|
||||
throw SOPGPException.CertUserIdNoMatch(errorMsg)
|
||||
}
|
||||
} catch (e: SOPGPException.CertUserIdNoMatch) {
|
||||
val errorMsg =
|
||||
if (e.fingerprint != null) {
|
||||
getMsg(
|
||||
"sop.error.runtime.cert_user_id_no_match",
|
||||
bytesToHex(e.fingerprint!!),
|
||||
userId)
|
||||
} else {
|
||||
getMsg("sop.error.runtime.any_cert_user_id_no_match", userId)
|
||||
}
|
||||
throw SOPGPException.CertUserIdNoMatch(errorMsg, e)
|
||||
} catch (e: SOPGPException.BadData) {
|
||||
val errorMsg = getMsg("sop.error.input.not_a_certificate", "STDIN")
|
||||
throw SOPGPException.BadData(errorMsg, e)
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
15
sop-java-picocli/src/main/resources/msg_armor.properties
Normal file
15
sop-java-picocli/src/main/resources/msg_armor.properties
Normal file
|
@ -0,0 +1,15 @@
|
|||
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
usage.header=Add ASCII Armor to standard input
|
||||
|
||||
standardInput=BINARY
|
||||
standardInputDescription=OpenPGP material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED)
|
||||
standardOutput=ARMORED
|
||||
standardOutputDescription=Same material, but with ASCII-armoring added, if not already present
|
||||
|
||||
stacktrace=Print stacktrace
|
||||
# Generic TODO: Remove when bumping picocli to 4.7.0
|
||||
usage.synopsisHeading=Usage:\u0020
|
||||
usage.optionListHeading=%nOptions:%n
|
||||
usage.footerHeading=Powered by picocli%n
|
13
sop-java-picocli/src/main/resources/msg_armor_de.properties
Normal file
13
sop-java-picocli/src/main/resources/msg_armor_de.properties
Normal file
|
@ -0,0 +1,13 @@
|
|||
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
usage.header=Schütze Standard-Eingabe mit ASCII Armor
|
||||
|
||||
standardInputDescription=OpenPGP Material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED)
|
||||
standardOutputDescription=Dasselbe Material, aber mit ASCII Armor kodiert, falls noch nicht geschehen
|
||||
|
||||
stacktrace=Stacktrace ausgeben
|
||||
# Generic TODO: Remove when bumping picocli to 4.7.0
|
||||
usage.synopsisHeading=Aufruf:\u0020
|
||||
usage.optionListHeading=%nOptionen:%n
|
||||
usage.footerHeading=Powered by Picocli%n
|
|
@ -0,0 +1,25 @@
|
|||
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
usage.header=Certify OpenPGP Certificate User IDs
|
||||
no-armor=ASCII armor the output
|
||||
userid=Identities that shall be certified
|
||||
with-key-password.0=Passphrase to unlock the secret key(s).
|
||||
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
|
||||
no-require-self-sig=Certify the UserID regardless of whether self-certifications are present
|
||||
KEYS[0..*]=Private keys
|
||||
|
||||
standardInput=CERTS
|
||||
standardInputDescription=Certificates that shall be certified
|
||||
standardOutput=CERTS
|
||||
standardOutputDescription=Certified certificates
|
||||
|
||||
picocli.endofoptions.description=End of options. Remainder are positional parameters. Fixes 'Missing required parameter' error
|
||||
|
||||
stacktrace=Print stacktrace
|
||||
# Generic TODO: Remove when bumping picocli to 4.7.0
|
||||
usage.parameterListHeading=%nParameters:%n
|
||||
usage.synopsisHeading=Usage:\u0020
|
||||
usage.commandListHeading=%nCommands:%n
|
||||
usage.optionListHeading=%nOptions:%n
|
||||
usage.footerHeading=Powered by picocli%n
|
|
@ -0,0 +1,22 @@
|
|||
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
usage.header=Zertifiziere OpenPGP Zertifikat Identitäten
|
||||
no-armor=Schütze Ausgabe mit ASCII Armor
|
||||
userid=Identität, die zertifiziert werden soll
|
||||
with-key-password.0=Passwort zum Entsperren der privaten Schlüssel
|
||||
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
|
||||
no-require-self-sig=Zertifiziere die Identität, unabhängig davon, ob eine Selbstzertifizierung vorhanden ist
|
||||
KEYS[0..*]=Private Schlüssel
|
||||
|
||||
standardInputDescription=Zertifikate, auf denen Identitäten zertifiziert werden sollen
|
||||
standardOutputDescription=Zertifizierte Zertifikate
|
||||
|
||||
picocli.endofoptions.description=Ende der Optionen. Der Rest sind Positionsparameter. Behebt 'Missing required parameter' Fehler
|
||||
|
||||
# Generic TODO: Remove when bumping picocli to 4.7.0
|
||||
usage.parameterListHeading=%nParameter:%n
|
||||
usage.synopsisHeading=Aufruf:\u0020
|
||||
usage.commandListHeading=%nBefehle:%n
|
||||
usage.optionListHeading=%nOptionen:%n
|
||||
usage.footerHeading=Powered by Picocli%n
|
|
@ -0,0 +1,26 @@
|
|||
# SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
usage.header=Update the password of a key
|
||||
usage.description.0=Unlock all secret keys from STDIN using the given old passwords and emit them re-locked using the new password to STDOUT.
|
||||
usage.description.1=If any (sub-) key cannot be unlocked, this operation will exit with error code 67.
|
||||
no-armor=ASCII armor the output
|
||||
new-key-password.0=New password to lock the keys with.
|
||||
new-key-password.1=If no new password is passed in, the keys will be emitted unlocked.
|
||||
new-key-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
|
||||
old-key-password.0=Old passwords to unlock the keys with.
|
||||
old-key-password.1=Multiple passwords can be passed in, which are tested sequentially to unlock locked subkeys.
|
||||
old-key-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
|
||||
|
||||
standardInput=KEYS
|
||||
standardInputDescription=OpenPGP keys whose passphrases shall be changed
|
||||
standardOutput=KEYS
|
||||
standardOutputDescription=OpenPGP keys with changed passphrases
|
||||
|
||||
stacktrace=Print stacktrace
|
||||
# Generic TODO: Remove when bumping picocli to 4.7.0
|
||||
usage.descriptionHeading=%nDescription:%n
|
||||
usage.synopsisHeading=Usage:\u0020
|
||||
usage.commandListHeading=%nCommands:%n
|
||||
usage.optionListHeading=%nOptions:%n
|
||||
usage.footerHeading=Powered by picocli%n
|
|
@ -0,0 +1,24 @@
|
|||
# SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
usage.header=Ändere das Passwort eines Schlüssels
|
||||
usage.description.0=Entsperre alle Schlüssel von Standard-Eingabe mithilfe der alten Passwörter und gebe sie mit dem neuen Passwort gesperrt auf Standard-Ausgabe aus.
|
||||
usage.description.1=Falls einer oder mehrere (Unter-)Schlüssel nicht entsperrt werden können, gibt diese Operation den Fehlercode 67 aus.
|
||||
no-armor=Schütze Ausgabe mit ASCII Armor
|
||||
new-key-password.0=Neues Passwort zur Sperrung der Schlüssel.
|
||||
new-key-password.1=Falls kein neues Passwort angegeben wird, werden die Schlüssel entsperrt ausgegeben.
|
||||
new-key-password.2=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
|
||||
old-key-password.0=Alte Passwörter zum Entsperren der Schlüssel.
|
||||
old-key-password.1=Mehrere Passwortkandidaten können übergeben werden, welche der Reihe nach durchprobiert werden, um Unterschlüssel zu entsperren.
|
||||
old-key-password.2=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
|
||||
|
||||
standardInputDescription=OpenPGP Schlüssel deren Passwörter geändert werden sollen
|
||||
standardOutputDescription=OpenPGP Schlüssel mit geänderten Passwörtern
|
||||
|
||||
stacktrace=Stacktrace ausgeben
|
||||
# Generic TODO: Remove when bumping picocli to 4.7.0
|
||||
usage.descriptionHeading=%nBeschreibung:%n
|
||||
usage.synopsisHeading=Aufruf:\u0020
|
||||
usage.commandListHeading=%nBefehle:%n
|
||||
usage.optionListHeading=%nOptionen:%n
|
||||
usage.footerHeading=Powered by Picocli%n
|
16
sop-java-picocli/src/main/resources/msg_dearmor.properties
Normal file
16
sop-java-picocli/src/main/resources/msg_dearmor.properties
Normal file
|
@ -0,0 +1,16 @@
|
|||
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
usage.header=Remove ASCII Armor from standard input
|
||||
|
||||
standardInput=ARMORED
|
||||
standardInputDescription=Armored OpenPGP material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED)
|
||||
standardOutput=BINARY
|
||||
standardOutputDescription=Same material, but with ASCII-armoring removed
|
||||
|
||||
stacktrace=Print stacktrace
|
||||
# Generic TODO: Remove when bumping picocli to 4.7.0
|
||||
usage.synopsisHeading=Usage:\u0020
|
||||
usage.commandListHeading=%nCommands:%n
|
||||
usage.optionListHeading=%nOptions:%n
|
||||
usage.footerHeading=Powered by picocli%n
|
|
@ -0,0 +1,14 @@
|
|||
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
usage.header=Entferne ASCII Armor von Standard-Eingabe
|
||||
|
||||
standardInputDescription=OpenPGP Material mit ASCII Armor (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED)
|
||||
standardOutputDescription=Dasselbe Material, aber mit entfernter ASCII Armor
|
||||
|
||||
stacktrace=Stacktrace ausgeben
|
||||
# Generic TODO: Remove when bumping picocli to 4.7.0
|
||||
usage.synopsisHeading=Aufruf:\u0020
|
||||
usage.commandListHeading=%nBefehle:%n
|
||||
usage.optionListHeading=%nOptionen:%n
|
||||
usage.footerHeading=Powered by Picocli%n
|
36
sop-java-picocli/src/main/resources/msg_decrypt.properties
Normal file
36
sop-java-picocli/src/main/resources/msg_decrypt.properties
Normal file
|
@ -0,0 +1,36 @@
|
|||
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
usage.header=Decrypt a message
|
||||
session-key-out=Can be used to learn the session key on successful decryption
|
||||
with-session-key.0=Symmetric message key (session key).
|
||||
with-session-key.1=Enables decryption of the "CIPHERTEXT" using the session key directly against the "SEIPD" packet.
|
||||
with-session-key.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
|
||||
with-password.0=Symmetric passphrase to decrypt the message with.
|
||||
with-password.1=Enables decryption based on any "SKESK" packets in the "CIPHERTEXT".
|
||||
with-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
|
||||
verify-out=Emits signature verification status to the designated output
|
||||
verify-with=Certificates for signature verification
|
||||
verify-not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z)
|
||||
verify-not-before.1=Reject signatures with a creation date not in range.
|
||||
verify-not-before.2=Defaults to beginning of time ('-').
|
||||
verify-not-after.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z)
|
||||
verify-not-after.1=Reject signatures with a creation date not in range.
|
||||
verify-not-after.2=Defaults to current system time ('now').
|
||||
verify-not-after.3=Accepts special value '-' for end of time.
|
||||
with-key-password.0=Passphrase to unlock the secret key(s).
|
||||
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
|
||||
KEY[0..*]=Secret keys to attempt decryption with
|
||||
|
||||
standardInput=CIPHERTEXT
|
||||
standardInputDescription=Encrypted OpenPGP message
|
||||
standardOutput=DATA
|
||||
standardOutputDescription=Decrypted OpenPGP message
|
||||
|
||||
stacktrace=Print stacktrace
|
||||
# Generic TODO: Remove when bumping picocli to 4.7.0
|
||||
usage.parameterListHeading=%nParameters:%n
|
||||
usage.synopsisHeading=Usage:\u0020
|
||||
usage.commandListHeading=%nCommands:%n
|
||||
usage.optionListHeading=%nOptions:%n
|
||||
usage.footerHeading=Powered by picocli%n
|
|
@ -0,0 +1,34 @@
|
|||
# SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
usage.header=Entschlüssle eine Nachricht
|
||||
session-key-out=Extrahiere den Nachrichtenschlüssel nach erfolgreicher Entschlüsselung
|
||||
with-session-key.0=Symmetrischer Nachrichtenschlüssel (Sitzungsschlüssel).
|
||||
with-session-key.1=Ermöglicht direkte Entschlüsselung des im "CIPHERTEXT" enthaltenen "SEIPD" Paketes mithilfe des Nachrichtenschlüssels.
|
||||
with-session-key.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
|
||||
with-password.0=Symmetrisches Passwort zur Entschlüsselung der Nachricht.
|
||||
with-password.1=Ermöglicht Entschlüsselung basierend auf im "CIPHERTEXT" enthaltenen "SKESK" Paketen.
|
||||
with-password.2=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
|
||||
verify-out=Schreibe Status der Signaturprüfung in angegebene Ausgabe
|
||||
verify-with=Zertifikate zur Signaturprüfung
|
||||
verify-not-before.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z)
|
||||
verify-not-before.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab.
|
||||
verify-not-before.2=Standardmäßig: Anbeginn der Zeit ('-').
|
||||
verify-not-after.0=Nach ISO-8601 formatierter UTC Zeitstempel (z.B.. '2020-11-23T16:35Z)
|
||||
verify-not-after.1=Lehne Signaturen mit Erstellungsdatum außerhalb des Gültigkeitsbereichs ab.
|
||||
verify-not-after.2=Standardmäßig: Aktueller Zeitpunkt ('now').
|
||||
verify-not-after.3=Akzeptiert speziellen Wert '-' für das Ende aller Zeiten.
|
||||
with-key-password.0=Passwort zum Entsperren der privaten Schlüssel
|
||||
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
|
||||
KEY[0..*]=Private Schlüssel zum Entschlüsseln der Nachricht
|
||||
|
||||
standardInputDescription=Verschlüsselte OpenPGP Nachricht
|
||||
standardOutputDescription=Entschlüsselte OpenPGP Nachricht
|
||||
|
||||
stacktrace=Stacktrace ausgeben
|
||||
# Generic TODO: Remove when bumping picocli to 4.7.0
|
||||
usage.parameterListHeading=%nParameter:%n
|
||||
usage.synopsisHeading=Aufruf:\u0020
|
||||
usage.commandListHeading=%nBefehle:%n
|
||||
usage.optionListHeading=%nOptionen:%n
|
||||
usage.footerHeading=Powered by Picocli%n
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue