Compare commits

...

519 commits
1.1.0 ... main

Author SHA1 Message Date
b28e234f21
ExternalTestSuite: Add license header 2025-07-25 14:09:57 +02:00
cc08b76b68
Introduce sop-java-json-gson module 2025-07-25 14:09:06 +02:00
e680f3450a
SOPV: Document since when operations are available 2025-07-25 12:21:06 +02:00
d4e8c14b08
Update documentation, add withKeyPassphrase(CharArray) methods 2025-07-24 12:53:27 +02:00
d32d9b54d7
Depend on junit-platform-suite to avoid needing to inherit test suite for external-sop tests 2025-07-01 15:06:50 +02:00
c651adc0b3
Add VerificationAssert methods, disable tests for unsupported operations 2025-07-01 14:38:13 +02:00
b3223372c6
SOP-Java 14.0.1-SNAPSHOT 2025-06-17 13:45:10 +02:00
9762f1f043
SOP-Java 14.0.0 2025-06-17 13:45:10 +02:00
191ec8c07d
Improve CHANGELOG again 2025-06-17 13:45:09 +02:00
07d0aa6941
Update CHANGELOG 2025-06-17 13:45:09 +02:00
12835bfb8e
Add/fix missing localizations for new SOP commands 2025-06-17 13:45:09 +02:00
04d154f63d
Version: Fix getSopJavaVersion() 2025-06-17 13:45:09 +02:00
01be696f75
Bump version to 14.0.0-SNAPSHOT 2025-06-17 13:45:09 +02:00
86718a2690
Bump logback to 1.5.13 2025-06-17 13:45:08 +02:00
e72e5a15c0
Fix: Pass chars to StringBuilder.append() 2025-06-17 13:45:08 +02:00
ac17000ff1
Clean up unused version literal 2025-06-17 13:45:08 +02:00
2a22cea29b
Remove animalsniffer 2025-06-17 13:45:08 +02:00
8a7fd5cb58
Move validate-userid to SOPV 2025-06-17 13:45:08 +02:00
21766a1f39
Delete ProxyOutputStream and test 2025-06-17 13:45:07 +02:00
cdcbae7e5f
Add test for JSON data parsing and serializing using a dummy implementation 2025-06-17 13:45:07 +02:00
ebfde35422
Add support for JSON POJOs 2025-06-17 13:45:07 +02:00
bff4423f93
Verification: Rename description to jsonOrDescription 2025-06-17 13:45:07 +02:00
5a7a8ae901
MergeCertsTest: do not pass unarmored data
This is done to fix external-sop tests, which rely on environment variables,
which do not play nicely with binary data
2025-06-17 13:45:07 +02:00
79aece6f04
SOP, SOPV: Add --debug option 2025-06-17 13:45:07 +02:00
28d06c330d
ExternalSOP: Map UnspecificError 2025-06-17 13:45:06 +02:00
ab13cc1de1
CertifyUserIdExternal: add separator before passing keys 2025-06-17 13:45:06 +02:00
e1d048225b
CertifyValidateUserIdTest: unbound User-IDs do throw exceptions 2025-06-17 13:45:06 +02:00
61206dde53
GenerateKeyTest: Provoke exception for CertCannotEncrypt test case 2025-06-17 13:45:06 +02:00
589884672a
External-SOP: Properly map KeyCannotCertify error code 2025-06-17 13:45:06 +02:00
47a6db8702
External-SOP: Fix error message typo 2025-06-17 13:45:05 +02:00
0df80470c6
External-SOP: Extend test suite with new test classes 2025-06-17 13:45:05 +02:00
00a02686c8
External-SOP: Fix command names 2025-06-17 13:45:05 +02:00
c5d9e57f69
Add test for encrypt-decrypt using all available generate-key profiles 2025-06-17 13:45:05 +02:00
e481717421
Add Profile.withAliases() utility function 2025-06-17 13:45:05 +02:00
9677f1fd0b
Fix profile constructors 2025-06-17 13:45:05 +02:00
e5cb58468b
Add aliases to Profile 2025-06-17 13:45:04 +02:00
4ef5444e78
Test key generation with supported profiles 2025-06-17 13:45:04 +02:00
77106942d1
Add tests for MergeCerts command 2025-06-17 13:45:04 +02:00
6d23d3771d
Remove unused import 2025-06-17 13:45:04 +02:00
9360b0e8ce
Remove println statements 2025-06-17 13:45:04 +02:00
38c5a947dd
Add MergeCertsTest 2025-06-17 13:45:03 +02:00
be460fabab
Add test for certifying without ASCII armor 2025-06-17 13:45:03 +02:00
138e275bb6
Fix formatting issues 2025-06-17 13:45:03 +02:00
091b5f9a5e
Add test for certifying with revoked key 2025-06-17 13:45:03 +02:00
dea7e905a9
Document update key 2025-06-17 13:45:03 +02:00
8c077a9c13
SOP update-key: Rename --no-new-mechanisms option to --no-added-capabilities 2025-06-17 13:45:03 +02:00
a8cfb8fbf4
Improve test 2025-06-17 13:45:02 +02:00
a8497617d5
Add basic test for certify-userid and validate-userid subcommands 2025-06-17 13:45:02 +02:00
68bab9cbb4
reuse: convert dep5 file to toml file 2025-06-17 13:45:02 +02:00
65aa0afd4e
Add new Exception types 2025-06-17 13:45:02 +02:00
a72545e3b9
Fix formatting 2025-06-17 13:45:02 +02:00
082cbde869
MergeCertsCmd: Fix default value of armor 2025-06-17 13:45:01 +02:00
b300be42a4
validate-userid: Add --validate-at option 2025-06-17 13:45:01 +02:00
5105b6f4ad
Remove call to explicitly set bundle to fix native image 2025-06-17 13:45:01 +02:00
7f1c1b1aae
Fix documentation of merge-certs command 2025-06-17 13:45:01 +02:00
4cf410a9f9
Bump version 2025-06-17 13:45:01 +02:00
f1bdce99cb
Document endOfOptionsDelimiter 2025-06-17 13:45:00 +02:00
f7cc9ab816
Fix nullability of sop commands 2025-06-17 13:45:00 +02:00
40ccb8cc99
Add first test for new commands 2025-06-17 13:45:00 +02:00
0bb50952c5
Show endOfOptions delimiter in help 2025-06-17 13:45:00 +02:00
69fbfc09a7
Implement external variants of new subcommands 2025-06-17 13:45:00 +02:00
dd12e28926
Checkstyle 2025-06-17 13:45:00 +02:00
4ed326a142
Implement validate-userid command 2025-06-17 13:44:59 +02:00
122cd016a1
Update msg files with input/output information 2025-06-17 13:44:59 +02:00
2b6a5dd651
Checkstyle and exception handling improvements 2025-06-17 13:44:59 +02:00
d6c1330874
Implement certify-userid command 2025-06-17 13:44:59 +02:00
ada77be955
Add support for rendering help info for input and output 2025-06-17 13:44:59 +02:00
84404d629f
Add implementation of merge-certs command 2025-06-17 13:44:58 +02:00
4115a5041d
Add implementation of update-key command 2025-06-17 13:44:58 +02:00
1dcf13244d
Add new exceptions 2025-06-17 13:44:58 +02:00
ad137d6351
Try to fix coveralls repo token 2025-05-15 02:16:52 +02:00
cbeec9c90d
SOP-Java 10.1.2-SNAPSHOT 2025-04-14 11:44:22 +02:00
701f9453ca
SOP-Java 10.1.1 2025-04-14 11:41:50 +02:00
2d99aea4ab
Bump animalsniffer to 2.0.0 2025-04-14 11:29:04 +02:00
4d2876a296
Fix formatting issue 2025-04-14 11:28:14 +02:00
e3fe9410d7
reuse: Migrate to toml format 2025-04-14 11:27:54 +02:00
a2a3bda2b3
Migrate AbortOnUnsupportedOption annotation back to java 2025-04-14 11:26:09 +02:00
cddc92bd92
Update changelog 2025-04-14 11:13:40 +02:00
8394f2e5a8
Make use of toolchain functionality and raise min Java API level to 11 2025-04-14 11:01:45 +02:00
2c26ab2da5
Improve reproducibility 2025-04-14 11:01:20 +02:00
859bb5bdde
Fix issues in kdoc 2025-04-04 12:16:00 +02:00
edb405d79e
Add TODO to remove ProxyOutputStream in 11.X 2025-04-04 12:11:20 +02:00
57e2f8391b
Update CHANGELOG 2025-04-04 10:43:49 +02:00
51ba24ddbe
Enable kapt annotation processing to properly embed picocli configuration files for native images into the -cli jar file
For this it is apparently necessary to upgrade kotlin to 1.9.21
See https://stackoverflow.com/a/79030947/11150851
2025-04-03 14:18:46 +02:00
d1893c5ea0
SOP-Java 10.1.1-SNAPSHOT 2025-03-11 21:22:31 +01:00
c145f8bb37
SOP-Java 10.1.0 2025-03-11 21:19:41 +01:00
924cfaa140
Update README 2025-03-11 21:18:24 +01:00
f2602bb413
Bump version to 10.1.0-SNAPSHOT 2025-03-11 21:04:50 +01:00
97e91f50ab
Migrate pipeline definition to use from_secret
https://woodpecker-ci.org/docs/usage/secrets\#use-secrets-in-settings-and-environment
2025-01-28 12:34:53 +01:00
690ba6dc16
Add rpgpie-sop reference in README 2025-01-28 12:34:53 +01:00
9ec3cc911b
Add bcsop reference in README 2025-01-28 12:34:53 +01:00
f92a73a5ad
Add back legacy --verify-out option alias for decrypt cmd 2025-01-28 12:34:53 +01:00
2b6015f59a
Add license header to properties files 2025-01-28 12:34:52 +01:00
84e381fe8e
Write sop-java version to sop-java-version.properties 2025-01-28 12:34:52 +01:00
b1e1a2283f
Update changelog 2025-01-28 12:34:51 +01:00
b3b8da4e35
Move testfixtures to own artifact 2025-01-28 12:33:07 +01:00
ca65cbe668
For now, do not re-set msg bundle (graal) 2025-01-28 12:33:06 +01:00
4eb6d1fdcb
Prevent unmatched parameters when setting locale 2025-01-28 12:33:06 +01:00
594b9029b2
Document logback spam 2025-01-28 12:33:06 +01:00
471947ef9c
Fix woodpecker warnings 2025-01-28 12:33:06 +01:00
1fd3161851
Properly match MissingArg exception code 2025-01-28 12:33:06 +01:00
a8a753536a
Add translations for new hardware exception error messages 2025-01-28 12:33:06 +01:00
eadea08d3c
Add new SOPGPException types related to hardware modules 2025-01-28 12:33:05 +01:00
547acdb740
Remove label() option from armor() operation 2025-01-28 12:33:05 +01:00
bb026bcbeb
Mark ProxyOutputStream as deprecated 2025-01-28 12:33:05 +01:00
e7778cb0d2
Remove deprecated junit5-system-exit
Replaced with custom test DSL that avoids System.exit
2025-01-28 12:33:05 +01:00
ac00b68694
Add description of external-sop module 2025-01-28 12:33:04 +01:00
e6c9d6f43d
SOP-Java 10.0.4-SNAPSHOT 2024-10-31 14:06:37 +01:00
c136d40fa7
SOP-Java 10.0.3 2024-10-31 13:54:31 +01:00
f35fd6c1ae
Update changelog 2024-10-31 13:53:57 +01:00
375dd65789
revoke-key command: Allow for multiple '--with-key-password' options 2024-10-31 13:48:55 +01:00
42a16a4f6d
Fix password parameter passing in change-key-password 2024-10-31 13:48:32 +01:00
b3f446fe8d
SOP-Java 10.0.3-SNAPSHOT 2024-10-14 16:22:06 +02:00
1958614fac
SOP-Java 10.0.2 2024-10-14 16:20:33 +02:00
a09f10fe85
Update changelog 2024-10-14 15:50:40 +02:00
a90f9be0e4
Downgrade logback-core to 1.2.13 2024-10-14 15:50:31 +02:00
63d8045224
Update changelog 2024-10-14 14:42:00 +02:00
7014dbcfb7
SOP-Java 10.0.2-SNAPSHOT 2024-10-14 14:36:41 +02:00
354ef8841a
SOP-Java 10.0.1 2024-10-14 13:46:48 +02:00
261ac212b8
Update changelog 2024-10-14 13:44:17 +02:00
f7530e3263
Bump logback to 1.4.14 2024-10-14 13:43:45 +02:00
8d7e89098f
Update changelog 2024-10-14 13:40:25 +02:00
a523270395
Update spec revision and badge link 2024-10-14 13:39:06 +02:00
d25a424adc
Update changelog 2024-10-14 13:34:56 +02:00
2d4bc24c64
Abort tests on UnsupportedOption 2024-10-14 13:32:28 +02:00
65945e0094
Fix external-sop decrypt --verifications-out 2024-10-14 13:32:11 +02:00
4388f00dc0
Fix NPE in DecryptExternal when reading lines 2024-10-14 12:36:26 +02:00
1df5747549
EncryptExternal: Fix parameter passing for --sign-with option 2024-10-14 12:33:58 +02:00
ae2389cabf
Bump version to 10.0.0 2024-03-17 18:06:50 +01:00
34a05e96a1
Move signature verification operations to sopv interface subset 2024-03-17 18:06:50 +01:00
7b04275625
Add test ckecking that BadData is thrown if KEYS is passed for CERTS 2024-03-17 18:06:50 +01:00
a0e7356757
Replace assumeTrue(false) with explicit TestAbortedException 2024-03-17 18:06:50 +01:00
173bc55eb9
Fix javadoc reference 2024-03-17 18:06:49 +01:00
03f8950b16
Rename woodpecker files 2024-03-17 18:05:35 +01:00
d5d7d67d6f
Fix reuse compliance 2024-03-17 17:58:04 +01:00
e2a568e73e
Update issue templates 2024-02-26 11:03:16 +01:00
7092baee4f
SOP-Java 8.0.2-SNAPSHOT 2023-11-22 18:21:41 +01:00
592aecd646
SOP-Java 8.0.1 2023-11-22 18:19:13 +01:00
e5e64003f3
decrypt: Do not throw NoSignature exception when verifications is empty 2023-11-22 17:23:06 +01:00
51d9c29837
decrypt --verify-with: Do not expect exit 3 when verifications is empty 2023-11-22 17:23:06 +01:00
ae83ddcff6
SOP-Java 8.0.1-SNAPSHOT 2023-11-15 19:01:50 +01:00
7eeb159f12
SOP-Java 8.0.0 2023-11-15 18:59:50 +01:00
60758dfa2f
Update changelog 2023-11-15 18:58:14 +01:00
6c952efca2
Fix NPE in line iterator 2023-11-15 18:50:34 +01:00
3eaae149b7
Kotlin conversion: VersionExternal 2023-11-15 18:34:30 +01:00
832a455c4c
Kotlin conversion: RevokeKeyExternal 2023-11-15 18:11:24 +01:00
f2204dfd4d
Kotlin conversion: ListProfilesExternal 2023-11-15 18:06:35 +01:00
8dc51b67a3
Kotlin conversion: InlineVerifyExternal 2023-11-15 17:59:04 +01:00
7be71494cf
Kotlin conversion: InlineSignExternal 2023-11-15 17:52:56 +01:00
f181453004
Kotlin conversion: InlineDetachExternal 2023-11-15 17:47:37 +01:00
9b79a49bb5
Kotlin conversion: GenerateKeyExternal 2023-11-15 17:41:33 +01:00
01abae4d08
Kotlin conversion: ExtractCertExternal 2023-11-15 17:36:00 +01:00
c53c69f3ac
Kotlin conversion: EncryptExternal 2023-11-15 17:32:43 +01:00
4a405f6d39
Kotlin conversion: DetachedVerifyExternal 2023-11-15 17:22:07 +01:00
9cd9f151c9
Kotlin conversion: DetachedSignExternal 2023-11-15 17:13:28 +01:00
03da9bbfb7
Kotlin conversion: DecryptExternal 2023-11-15 17:04:00 +01:00
da2b299f4d
Kotlin conversion: DearmorExternal 2023-11-15 16:45:32 +01:00
d149aac56c
Kotlin conversion: ChangeKeyPasswordExternal 2023-11-15 16:45:21 +01:00
6771952618
Kotlin conversion: ArmorExternal 2023-11-15 16:33:44 +01:00
1c0666b4e1
Kotlin conversion: ExternalSOP 2023-11-15 16:27:19 +01:00
d24ff9cbde
Remove label related CLI tests 2023-11-15 15:26:06 +01:00
802bc0aa73
ArmorCMD: Drop --label option 2023-11-15 15:24:09 +01:00
03cabdf3fb
Add tests for --no-armor for change-key-password and revoke-key 2023-11-15 15:22:40 +01:00
3dde174880
Fix woodpecker pipeline 2023-11-15 14:26:24 +01:00
2051c3632a
external-sop: Mark methods with @Nonnull where applicable 2023-11-15 13:52:36 +01:00
0563105b1f
Bump version to 8.0.0-SNAPSHOT 2023-11-15 13:38:02 +01:00
72ca392386
Merge remote-tracking branch 'origin/sop08' 2023-11-15 13:03:07 +01:00
a5c332737b
Merge pull request #23 from pgpainless/kotlinCmd
Rewrite sop-java-picocli classes in Kotlin
2023-11-15 12:55:10 +01:00
41acdfe03a
ProxyOutputStream: Extend OutputStream 2023-11-15 12:42:56 +01:00
edef899074
Fix GenerateKey --with-key-password option name 2023-11-15 12:42:41 +01:00
baa44a6b1a
Kotlin conversion: SopCLI 2023-11-15 12:35:23 +01:00
0c2cf5cb19
Kotlin conversion: SOPExecutionExceptionHandler 2023-11-15 11:59:11 +01:00
5c2695228b
Kotlin conversion: SOPExceptionExitCodeMapper 2023-11-15 11:55:50 +01:00
b251956f49
Delete unused Print class 2023-11-15 11:49:45 +01:00
b884f2b1a9
Kotlin conversion: InlineVerifyCmd 2023-11-15 11:49:04 +01:00
2e118357e2
Kotlin conversion: InlineSignCmd 2023-11-15 11:40:46 +01:00
e9a5467f6b
Kotlin conversion: GenerateKeyCmd 2023-11-15 11:33:34 +01:00
019dd63e1b
Kotlin conversion: ExtractCertCmd 2023-11-15 11:26:06 +01:00
bfad8c4203
Kotlin conversion: EncryptCmd 2023-11-15 11:21:42 +01:00
159ffbe084
Add missing license headers 2023-11-04 18:32:01 +01:00
714c933cef
Kotlin conversion: InlineDetachCmd 2023-11-04 18:31:52 +01:00
9daabb758a
Kotlin conversion: DecryptCmd 2023-11-04 18:18:55 +01:00
8e65771e36
Kotlin conversion: ListProfilesCmd 2023-11-04 17:50:07 +01:00
688b8043a2
Kotlin conversion: DearmorCmd 2023-11-04 17:45:56 +01:00
49120c5da8
Kotlin conversion: ChangeKeyPasswordCmd 2023-11-04 17:41:07 +01:00
377a7287b3
Kotlin conversion: ArmorCmd 2023-11-04 17:34:30 +01:00
18865feaff
Kotlin conversion: RevokeKeyCmd 2023-11-04 17:26:38 +01:00
666d51384b
Kotlin conversion: AbstractSopCmd 2023-11-04 17:20:00 +01:00
256d1c5960
Kotlin conversion: SignCmd 2023-11-04 16:34:35 +01:00
8246359a85
Kotlin conversion: VerifyCmd 2023-11-04 16:15:35 +01:00
1de179c015
Kotlin conversion: VersionCmd 2023-11-04 16:07:37 +01:00
86b173bf1c
Update Changelog 2023-10-31 16:16:32 +01:00
5ee9414410
Encrypt: Add --session-key-out support 2023-10-31 16:00:04 +01:00
a8829350a8
Merge pull request #21 from pgpainless/kotlin
Rewrite sop-java in Kotlin
2023-10-31 15:59:36 +01:00
7824ee92c5
Kotlin conversion: SOPGPException 2023-10-31 15:27:47 +01:00
94b428ef62
Kotlin conversion: UTF8Util 2023-10-31 15:18:48 +01:00
e1a6ffd07a
Use @JvmField annotation 2023-10-31 15:18:39 +01:00
25a33611fd
Kotlin conversion: UTCUtil 2023-10-31 15:10:46 +01:00
05886228df
Kotlin conversion: ProxyOutputStream 2023-10-31 15:07:13 +01:00
b7007cc007
Kotlin conversion: HexUtil 2023-10-31 15:00:21 +01:00
01f98df80b
Kotlin conversion: SignatureMode 2023-10-31 14:52:45 +01:00
30c369d24a
Kotlin conversion: SignAs 2023-10-31 14:51:16 +01:00
be6be3deac
Kotlin conversion: InlineSignAs 2023-10-31 14:50:09 +01:00
1c290e0c8f
Kotlin conversion: EncryptAs 2023-10-31 14:48:48 +01:00
d5c0d4e390
Kotlin conversion: ArmorLabel 2023-10-31 14:47:14 +01:00
4b9e2c206f
Fix DecryptionResult constructor 2023-10-31 14:46:41 +01:00
049c18c17b
Fix sop-java-picocli jar task 2023-10-31 14:45:56 +01:00
d0ee9c2066
Kotlin conversion: Version 2023-10-31 14:38:34 +01:00
a8c2e72ef5
Kotlin conversion: VerifySignatures 2023-10-31 14:30:24 +01:00
0ee4638beb
Kotlin conversion: RevokeKey 2023-10-31 14:28:59 +01:00
145cadef4f
Kotlin conversion: ListProfiles 2023-10-31 14:27:17 +01:00
6c14f249bb
Kotlin conversion: InlineVerify 2023-10-31 14:25:47 +01:00
be0ceb0886
Kotlin conversion: InlineSign 2023-10-31 14:25:25 +01:00
9283f81c56
Replace ByteArrayInputStream with inputStream() 2023-10-31 14:22:12 +01:00
8df4a520bd
Kotlin conversion: InlineDetach 2023-10-31 14:20:29 +01:00
3e6ebe1cc4
Kotlin conversion: GenerateKey 2023-10-31 14:19:08 +01:00
653675f730
Kotlin conversion: ExtractCert 2023-10-31 14:17:12 +01:00
41db9d2ac7
Kotlin conversion: Encrypt 2023-10-31 14:15:56 +01:00
e681090757
Kotlin conversion DetachedVerify 2023-10-31 14:12:56 +01:00
ee6975c7d3
Decrypt: Use return statement 2023-10-31 14:11:11 +01:00
4dc1779a06
Kotlin conversion: DetachedSign 2023-10-31 14:10:06 +01:00
91a861b5c3
Kotlin conversion: Decrypt 2023-10-31 14:08:03 +01:00
39c222dfc8
Kotlin conversion: Dearmor 2023-10-31 14:03:18 +01:00
34e1d8992f
Kotlin conversion: ChangeKeyPassword 2023-10-31 14:00:49 +01:00
4a123a1980
Kotlin conversion: Armor 2023-10-31 13:54:24 +01:00
08ddc5d8a5
Kotlin conversion: AbstractVerify 2023-10-31 13:51:30 +01:00
e68d6df57f
Kotlin conversion: AbstractSign 2023-10-31 13:50:46 +01:00
31409b7949
Kotlin conversion: SOP 2023-10-31 13:38:53 +01:00
dc23c8aa98
Kotlin conversion: Signatures 2023-10-31 13:29:58 +01:00
2391ffc9b2
Kotlin conversion: DecryptionResult 2023-10-31 13:26:33 +01:00
a89e70c19e
Kotlin conversion: ReadyWithResult 2023-10-31 13:23:26 +01:00
e6562cecff
Kotlin conversion: Ready 2023-10-31 13:16:37 +01:00
9dbb93e13d
Kotlin conversion: MicAlg 2023-10-31 13:05:30 +01:00
bbe159e88c
Kotlin conversion: SigningResult 2023-10-31 12:56:12 +01:00
0cb5c74a11
Kotlin conversion: Optional 2023-10-31 12:48:23 +01:00
ef4b01c6bd
Kotlin conversion: Profile 2023-10-31 12:42:13 +01:00
6c5c4b3d98
Kotlin conversion: Verification 2023-10-31 12:25:54 +01:00
567571cf6c
Kotlin conversion: SessionKey 2023-10-31 11:33:15 +01:00
0f5270c28d
Kotlin conversion: ByteArrayAndResult 2023-10-31 11:24:36 +01:00
4bd4657906
Add kotlin plugin 2023-10-31 11:24:27 +01:00
cf1d39643d
SOP-Java 7.0.1-SNAPSHOT 2023-07-12 15:44:05 +02:00
f2073dcbf4
SOP-Java 7.0.0 2023-07-12 15:41:47 +02:00
308c4b452f
Add test for signature verification with hard-revoked cert 2023-07-12 15:36:39 +02:00
be351616b6
Add some more RevokeKey tests 2023-07-12 15:08:33 +02:00
530c44ad16
Typos and improvements 2023-07-12 14:57:51 +02:00
9ad59abb2a
Improve description of extract-cert 2023-07-12 14:30:45 +02:00
cd2c62ce2b
Improve description of change-key-password 2023-07-12 14:30:23 +02:00
edb384d157
Reference exit codes by Exception 2023-07-12 14:22:57 +02:00
feb9efc733
Reorder subcommands 2023-07-12 14:20:53 +02:00
009364b217
Add missing i18n and fix broken strings 2023-07-12 14:17:28 +02:00
f13aade98e
Add missing parameter labels 2023-07-12 14:15:47 +02:00
d1c614344c
Update i18n 2023-07-12 01:35:25 +02:00
6a579bff03
Update README 2023-07-12 01:13:01 +02:00
9ba005f7cc
Update changelog 2023-07-12 01:10:47 +02:00
6afe6896d8
Implement '--signing-only' option for 'generate-key' command 2023-07-12 01:06:41 +02:00
7e1377a28c
Initial implementation of 'change-key-password' command of SOP-07 2023-07-12 00:42:02 +02:00
618d123a7b
Add RevokeKeyExternal implementation and some basic tests 2023-07-11 22:58:39 +02:00
e6393b44b9
Initial implementation of 'revoke-key' command 2023-07-11 22:42:16 +02:00
ab2e4aa8e7
Bump version to 7.0.0-SNAPSHOT 2023-07-11 22:41:58 +02:00
f65ddba4b4
Update changelog 2023-07-11 21:27:51 +02:00
bfaba69222
Add Dearmor.data(String) default method 2023-07-11 21:18:37 +02:00
7ab65f63a4
SOP-Java 6.1.1-SNAPSHOT 2023-04-27 14:49:13 +02:00
b8ad6d77a2
SOP-Java 6.1.0 2023-04-27 14:46:52 +02:00
ab8f44138d
Add missing @throws javadoc 2023-04-27 14:45:42 +02:00
419056ba4c
Fix checkstyle issues 2023-04-27 14:32:13 +02:00
312cdb69c9
Update changelog 2023-04-27 14:28:07 +02:00
c479cc8ef3
Profile: Use Optional for description 2023-04-27 14:25:16 +02:00
aa88904711
Add tests for Verification parsing 2023-04-27 14:24:59 +02:00
7ea46a1916
Move tests 2023-04-27 14:23:58 +02:00
49fd7143cf
Update CHANGELOG 2023-04-27 13:26:01 +02:00
8aded17f10
Use UTF8Util.UTF8 constant 2023-04-27 13:17:58 +02:00
8eba099146
UTF8Util.decodeUTF8(): throw CharacterCodingException instead of PasswordNotHumanReadable 2023-04-27 13:15:44 +02:00
0308732328
Make UTCUtil.parseUTCDate() throw instead of returning null for malformed inputs 2023-04-27 13:07:08 +02:00
8b8863c6df
Verification: Annotate with @Nonnull, @Nullable 2023-04-27 12:52:12 +02:00
44e6dd2180
Depend on findbugs:jsr305 for @Nullable etc. annotations 2023-04-27 12:44:40 +02:00
19d6b7e142
Verification: Make use of Optional for signature mode and description 2023-04-27 12:44:01 +02:00
226b5d99a0
Fix issues with i18n properties 2023-04-26 16:47:53 +02:00
e336e536a8
Javadoc: Insert <p> tags to preserve newlines 2023-04-26 16:44:49 +02:00
ed59c713eb
Remove unused 'throws IOException' declarations 2023-04-26 16:28:04 +02:00
0aabfac695
DetachedVerifyExternal: Make certs set final 2023-04-26 16:23:18 +02:00
790d80ec29
DateParsingTest: make armor command final 2023-04-26 16:22:53 +02:00
0fccf3051c
Refactor AbstractSOPTest 2023-04-26 16:21:37 +02:00
a722e98578
Update changelog 2023-04-26 16:12:32 +02:00
aeda534f37
Add DSL for testing verification results 2023-04-26 15:53:50 +02:00
bb2b4e03fb
Add comment to remind future self of adding new exception types to switch-case 2023-04-18 18:15:11 +02:00
4a7c2b74da
Add test for listing encrypt profiles, use new shortcut methods 2023-04-18 18:05:10 +02:00
78ecf2f554
ListProfiles: Add shortcut methods generateKey() and encrypt() 2023-04-18 18:04:23 +02:00
d8cac7b9d7
external-sop: Fix error code mapping of new exceptions 2023-04-18 18:03:18 +02:00
51a7d950e5
SOP-Java 6.0.1-SNAPSHOT 2023-04-17 15:56:06 +02:00
41260bb02c
SOP-Java 6.0.0 2023-04-17 15:45:37 +02:00
415fc3dd3a
Update changelog 2023-04-17 15:43:01 +02:00
84a01df4bd
Rename new methods in Version 2023-04-17 15:29:06 +02:00
1a4affde35
Skip sopSpecVersionTest if --sop-spec is not supported 2023-04-17 14:44:09 +02:00
8425665fa7
Expose SOP revision info more fine-grained 2023-04-17 14:44:09 +02:00
90c77706a8
Add i18n for profile option 2023-04-17 14:44:08 +02:00
8a66f0bc4f
Implement sop encrypt --profile=XXX 2023-04-17 14:44:08 +02:00
f4ff5f89f7
i18n for sop version --sop-spec 2023-04-17 14:44:08 +02:00
f49c16e4c5
Implement sop version --sop-spec 2023-04-17 14:44:08 +02:00
dfce1ad6bb
Bump SOP spec to 06 2023-04-17 14:44:08 +02:00
925505989e
Bump version to 6.0.0 2023-04-17 14:44:06 +02:00
42bd8f06a4
SOP-Java 5.0.1-SNAPSHOT 2023-04-17 14:41:39 +02:00
3f4ec072a9
SOP-Java 5.0.0 2023-04-17 14:39:23 +02:00
146f24eab8
Update changelog 2023-04-17 14:21:34 +02:00
a3bff6f6d1
Fix checkstyle issues 2023-04-17 14:12:01 +02:00
8a9f535531
Add documentation to ListProfiles command 2023-04-17 14:04:06 +02:00
7e12da400b
Add documentation to new exception types 2023-04-17 14:01:49 +02:00
360f2fba02
Document SignatureMode 2023-04-17 13:56:43 +02:00
d838e9589b
Add documentation to Verification 2023-04-17 13:51:05 +02:00
ffdd5eee51
Document SOP.listProfiles() 2023-04-17 13:41:03 +02:00
67292864b3
Add documentation to Profile class 2023-04-17 13:39:36 +02:00
5d2f87eb80
msg_sop.properties: Add newline at the end 2023-04-17 13:16:13 +02:00
6ec62560ba
Add comment about why --as=clearsigned and --no-armor are incompatible 2023-04-17 13:13:36 +02:00
7743f15e72
ListProfilesExternal: make toProfiles() method static 2023-04-17 13:10:30 +02:00
9bc391fc7c
Add note discouraging reuse of subcommand objects 2023-04-17 13:07:54 +02:00
64c0fb11bc
ListProfilesCmd: Replace System.out.println() with Print.outln() 2023-04-17 13:03:18 +02:00
d38556f79a
GenerateKeyCmd: Do not assert default profile 2023-04-14 15:06:07 +02:00
19663c4dec
Update SOP revision 2023-04-14 14:21:52 +02:00
1e805db1f0
Bump version to 5.0.0 2023-04-14 14:19:22 +02:00
dff5b01eec
Add some documentation to AbstractSopCmd 2023-04-14 14:06:49 +02:00
5935d65c90
Implement profiles 2023-04-14 14:06:34 +02:00
b8544396f8
Add Profile class 2023-04-14 14:03:22 +02:00
0ed5c52f4b
Change ListProfiles class to interface 2023-04-11 15:27:23 +02:00
83a003e80f
Add list-profiles command 2023-04-11 15:06:37 +02:00
17b305924c
Throw IncompatibleOptions error for sign --as=clearsigned --no-armor 2023-04-09 19:53:21 +02:00
6d28a7b07d
Add new Exceptions 2023-04-09 19:52:34 +02:00
f5e34bce6c
Fix IOOB in Verification.fromString() 2023-04-09 19:51:49 +02:00
5d04bb965b
Add support for 05-style verifications 2023-04-09 19:29:10 +02:00
ae64414492
Decide when to remove --verify-out 2023-04-09 19:00:35 +02:00
f37354d268
Fix reuse 2023-01-31 18:50:41 +01:00
6448debf46
SOP-Java 4.1.2-SNAPSHOT 2023-01-31 18:45:26 +01:00
d488eee36f
SOP-Java 4.1.1 2023-01-31 18:41:13 +01:00
9fdc8a5bad
Improve comment on external-sop/build.gradle 2023-01-31 18:40:38 +01:00
546b97fcc9
Fix checkstyle error 2023-01-31 18:40:14 +01:00
40dc9e3707
Move gson version to version.gradle 2023-01-31 18:36:05 +01:00
6ac133499c
Enable tests only if test backends are available 2023-01-31 18:35:48 +01:00
6fad442cd0
Update changelgo 2023-01-31 18:25:59 +01:00
0709bce35c
Allow for extension of test suite from 3rd party SOP implementations 2023-01-31 18:20:27 +01:00
fd426b533c
Restructure test artifacts into sop-java testFixtures 2023-01-27 00:35:38 +01:00
88e3ba0095
Add section about license compliance and testing to external-sop/README 2023-01-22 17:37:47 +01:00
6c3e148bcd
Update changelog 2023-01-22 17:35:48 +01:00
c1ae5314a0
CI: Test external-sop against sqop 2023-01-22 17:07:17 +01:00
3789b60f0b
Properly ignore tests if no backends are configured 2023-01-22 16:53:50 +01:00
0b96a5314f
Restructure external-sop tests into flexible test suite 2023-01-22 16:47:44 +01:00
0c8f6baf98
Only swallow 'Stream closed' IOExceptions 2023-01-22 15:37:27 +01:00
8cacf7dd57
Code cleanup 2023-01-22 15:07:17 +01:00
e73c7e5f91
Even more tests 2023-01-21 21:17:57 +01:00
9cf6301b8c
More tests 2023-01-21 20:31:49 +01:00
d09626782d
Unify tests by turning password-protected keys into variable 2023-01-20 15:16:47 +01:00
c95ca8fedc
Add test for signing with protected key without password 2023-01-20 14:58:21 +01:00
0d9db2bdd3
Add tests for extracting certs from known keys 2023-01-20 14:37:58 +01:00
ffc5b26c0d
Add test for unsupported subcommands 2023-01-20 14:30:41 +01:00
61c5eb2962
Add javadoc to JUtils 2023-01-19 18:10:31 +01:00
104b3a4ec4
Add documentation and fixes to AbstractExternalSOPTest 2023-01-19 18:00:41 +01:00
0616cde6fd
Add documentation of the ExternalSOP class 2023-01-19 17:51:29 +01:00
990d314709
Add javadoc comments on top of external sop operations 2023-01-19 17:29:29 +01:00
4fc8ffab42
SOP-Java 4.1.1-SNAPSHOT 2023-01-13 19:08:18 +01:00
78b5eea630
SOP-Java 4.1.0 2023-01-13 19:06:19 +01:00
b42d0e89a1
Update CHANGELOG 2023-01-13 19:03:33 +01:00
8fc88b5bab Increase test coverage 2023-01-13 18:53:14 +01:00
eded55c259 Add more tests 2023-01-13 18:53:14 +01:00
4726362df8 Add more armor/dearmor tests 2023-01-13 18:53:14 +01:00
670aa0f242 Add test for dearmoring and armoring certificate 2023-01-13 18:53:14 +01:00
125eefed6e Fix IOException when trying to close already-closed output stream 2023-01-13 18:53:14 +01:00
6e40c7dc17 Add initial tests for all operations 2023-01-13 18:53:14 +01:00
bd02b11944 Fix external version command 2023-01-13 18:53:14 +01:00
951cf9cbca Fix external armor and dearmor commands 2023-01-13 18:53:14 +01:00
b15acc79b3 Update README 2023-01-13 18:53:14 +01:00
f1c6fd67d3 Update sop-java/README 2023-01-13 18:53:14 +01:00
85f61b413f Add external-sop/README.md 2023-01-13 18:53:14 +01:00
9c27141c00 Add documentation to TempDirProvider 2023-01-13 18:53:14 +01:00
909e28432d Wip: Implement result parsing for decrypt, inline-detach and inline-verify 2023-01-13 18:53:14 +01:00
d079a345d2 decrypt: Parse out sessionkey and verifications, sign: parse out micalg 2023-01-13 18:53:14 +01:00
e3b618a0a8 Wip: Add TempDirProvider 2023-01-13 18:53:14 +01:00
3eb2503852 Add back missing exception constructor 2023-01-13 18:53:14 +01:00
a63b29fe80 WiP: Implement first prototypes of all SOP commands for external binaries 2023-01-13 18:53:14 +01:00
efec4d9110 Wip: working extract-cert, fix generate-key parameter passing 2023-01-13 18:53:14 +01:00
e602cc16cc Rename module to external-sop and make backend in tests configurable 2023-01-13 18:53:14 +01:00
28912618ea Initial work on binary-sop
This module is intended to allow the use of SOP command line applications
such as sqop, pgpainless-sop, etc. as drop-ins for sop-java.
2023-01-13 18:53:14 +01:00
ed296ec4b2
Update changelog 2023-01-13 18:52:13 +01:00
3bc19b27ad toString() of enum options: Ensure lowercase 2023-01-13 18:50:36 +01:00
eddcc11c99
Update changelgo 2023-01-13 17:45:15 +01:00
ff5f98e8ee sop decrypt: Throw NoSignature if no verifiable signature found 2023-01-13 17:41:20 +01:00
14c665565e
Update changelog 2023-01-12 15:52:16 +01:00
d9708e882d
decrypt: rename --not-after, --not-before to --verify-not-after, --verify-not-before 2023-01-12 14:03:12 +01:00
2c3717157a
Bump gradlew to 7.5 2023-01-05 01:42:31 +01:00
2328bdf6af
Fix parameter label of --as=clearsigned 2022-12-13 17:06:35 +01:00
ae128d2cbb
SOP-Java 4.0.8-SNAPSHOT 2022-11-23 20:18:29 +01:00
55f196b241
SOP-Java 4.0.7 2022-11-23 20:16:14 +01:00
62d3ffd9ae
Update changelog 2022-11-23 20:10:47 +01:00
519fb891a1
Dearmor: transform IOExceptions into BadData properly 2022-11-18 15:17:33 +01:00
0e777de14f
Make asciiDoctor gradle task generate reproducible output 2022-11-15 21:43:43 +01:00
0ed7163fd5
Fix capitalization of @ENV/@FD descriptions 2022-11-15 21:29:45 +01:00
ed46adbe52
Add resource string for --stacktrace option 2022-11-15 15:49:06 +01:00
60f52ab89e
SOP-Java 4.0.7-SNAPSHOT 2022-11-15 15:32:38 +01:00
3a6905c0bd
SOP-Java 4.0.6 2022-11-15 15:30:31 +01:00
00ab68b504
Allow larger numbers for file descriptor names 2022-11-14 13:54:08 +01:00
1a381becfa
Update changelog 2022-11-11 15:59:41 +01:00
4bc45a0692
Add support for using file descriptors as input and output
Fixes #12
2022-11-11 15:56:33 +01:00
3ee42ea2ed
SOP-Java 4.0.6-SNAPSHOT 2022-11-11 13:24:07 +01:00
114ee94f0d
SOP-Java 4.0.5 2022-11-11 13:22:13 +01:00
82456ec9e1 Update CHANGELOG 2022-11-11 12:48:31 +01:00
c00109c2bf Rename InlineSignAs.CleartextSigned to clearsigned, make options lowercase
Fixes #16
2022-11-11 12:48:26 +01:00
01beb99e43
Add packaging badges to README 2022-11-07 17:26:05 +01:00
6f20f78339
SOP-Java 4.0.5-SNAPSHOT
4.0.4 Not Found
2022-11-07 16:46:38 +01:00
76cc9098ce
SOP-Java 4.0.3 2022-11-07 16:44:33 +01:00
7326d3cf85
Add tests for writing out of session key and verifications 2022-11-07 16:21:50 +01:00
137d2e7f85
Add Verification.fromString(string), equals(other) and hashCode() 2022-11-07 16:21:34 +01:00
dad75bb522
Clean up toString() method 2022-11-07 16:21:01 +01:00
9ea9cd22b8
Fix writing out of session key - again 2022-11-07 16:20:37 +01:00
5cde383b1a
Update changelog 2022-11-07 15:32:54 +01:00
f60e14de96
Bump gradlew to 7.2 2022-11-07 01:18:51 +01:00
8494f413e6
inline-verify: flush printwriter of --verifications-out 2022-11-07 01:17:54 +01:00
aa953428ee Be less finnicky about session key formats 2022-11-06 20:30:03 +01:00
dd5d790e21
Properly format session keys for --session-key-out 2022-11-06 20:22:40 +01:00
40919be3f7
sop decrypt: rename --verify-out to --verifications-out
--verify-out is still available as an alias.
See https://gitlab.com/dkg/openpgp-stateless-cli/-/issues/55
2022-11-06 19:02:37 +01:00
4ba864e1cd
Bump asciidoctor gradle plugin to 3.3.2 2022-11-06 15:12:00 +01:00
46e62e9e40
SOP-Java 4.0.3-SNAPSHOT 2022-11-06 14:58:18 +01:00
60ef0a15a8
SOP-Java 4.0.2 2022-11-06 14:56:36 +01:00
1aaac4e4b9
Update CHANGELOG 2022-11-06 14:43:22 +01:00
fd4c22cde6
Add --stacktrace option 2022-11-06 14:41:52 +01:00
c32ef9830b
Properly throw CannotDecrypt exception with error message 2022-11-06 14:41:37 +01:00
e75dde1637
Remove colons from error message 2022-11-06 14:40:53 +01:00
162ae120c3
Update changelog 2022-11-05 18:11:02 +01:00
043e95e5c0 Fix arity of arguments in verify commands
Fixes https://github.com/pgpainless/pgpainless/issues/330
2022-11-05 18:06:53 +01:00
04d38b988a Merge branch 'manPages' 2022-08-08 00:15:05 +02:00
29660a1d3f
SOP-Java 4.0.2-SNAPSHOT 2022-08-04 13:59:06 +02:00
a14f008215
SOP-Java 4.0.1 2022-08-04 13:57:41 +02:00
06d92eecf3
Update changelog 2022-08-04 13:56:16 +02:00
e17d4d2efb
Add shared resources to all subcommand bundles 2022-08-04 13:14:21 +02:00
092a83a1e7 Merge branch 'sharedResource' 2022-08-04 12:51:32 +02:00
0cb614827b
Un-bump picocli 2022-08-04 12:51:01 +02:00
dcb44f96c8
Fix wrongly named resource entries 2022-08-04 12:48:32 +02:00
dc5f11469f
Rename resource bundles and name resources after options/parameters 2022-08-04 12:15:53 +02:00
d80a0a067f
Sign: Test that key passwords are passed down from CLI 2022-08-01 18:46:11 +02:00
c4cbf8ff69
Add tests for getOutput 2022-08-01 18:31:56 +02:00
fe729c4eb8
Add AbstractSopCmdTest to test getInput method 2022-08-01 18:21:41 +02:00
b7c1b4f1a1
Remove unused methods from Print class 2022-08-01 17:44:22 +02:00
01dbaab598
Delete .travis.yml 2022-08-01 17:41:03 +02:00
a651abb44e
Replace badges for Travis-CI with Codeberg-CI and fix coverage badge 2022-08-01 17:09:39 +02:00
ed150e51a8
Woodpecker: Attempt to fix branch name 2022-08-01 16:40:18 +02:00
1e9b1c77d3
Clean up woodpecker scripts 2022-08-01 16:09:41 +02:00
5fb1146dfb
Add .woodpecker/ for codeberg-ci 2022-08-01 16:03:46 +02:00
8c581d459a
WIP: Add task for generating man pages 2022-07-27 21:40:24 +02:00
7de94f1815
Set resource bundle for help command 2022-07-27 14:30:48 +02:00
fa52df385e
Split message resources into separate per-command resource files.
Since picocli 4.7.0, subcommands inherit resources from their
parent commands, so we can store shared stuff like error msgs
etc. in the parent (sop) resources file.

This enables us to rename the parent command downstream (e.g. in
pgpainless-cli).

Only the help command breaks when renaming the parent command.
TODO: Fix
2022-07-25 19:15:47 +02:00
3801a644ef
Do not overwrite command name.
Doing so would break resolution of command.usage.header strings
2022-07-23 01:21:14 +02:00
86e39809ae
Fix sop.properties sop command header 2022-07-23 01:11:40 +02:00
77c76c57d0
Add InlineDetachCmdTest 2022-06-20 19:50:50 +02:00
084923c828
SOP-Java 4.0.1-SNAPSHOT 2022-06-19 18:24:54 +02:00
bfce786433
SOP-Java 4.0.0 2022-06-19 18:23:19 +02:00
1ca1bb5671 Update CHANGELOG 2022-06-19 18:19:23 +02:00
55673b52a7 Fix typos in i18n 2022-06-19 18:19:23 +02:00
8db710a582
Update SOP version in README 2022-06-19 18:00:54 +02:00
61ab35ad52
Reference exit codes directly in junit tests 2022-06-11 11:18:45 +02:00
0c24c2301d
Add KeyCannotSign(message) constructor 2022-06-10 16:55:53 +02:00
eefb445916
Reformat and restructure exceptions 2022-06-10 16:16:37 +02:00
e175ee6208
Fix return types of sign builders 2022-06-09 00:46:40 +02:00
6d5005660c
Fix format strings 2022-06-09 00:46:25 +02:00
2135455cda
Don't throw KeyIsProtected 2022-06-07 08:55:44 +02:00
4934e472e2
Wip: Refactor command implementation and enable i18n for CLI 2022-06-06 20:06:14 +02:00
a7f02d58cc
Wip 2022-05-29 21:17:03 +02:00
8184c30617
GenerateKeyCmd: Make --with-key-password indirect data type 2022-05-24 22:13:11 +02:00
45ee435a18
Add support for --with-key-password to DecryptCmd 2022-05-24 22:10:21 +02:00
6438ebc59c
Make Sign.withKeyPassword indirect data type 2022-05-24 22:09:49 +02:00
de11c17967
Rename DetachInbandSignaturesAndMessageCmd to InlineDetachCmd 2022-05-24 21:56:17 +02:00
55aefb799f
Add support for --with-key-password to Sign 2022-05-24 21:55:35 +02:00
8406793d0e
Add param label for PASSWORD 2022-05-24 21:53:14 +02:00
9016c9f428
SOP: Add support for key password in GenerateKeyCmd 2022-05-24 21:49:40 +02:00
580c3af350
Add InlineSign API 2022-05-24 21:44:46 +02:00
e8d7d1b5f4
Move withKeyPassword method in Sign around 2022-05-24 21:36:02 +02:00
72c3b3218d
Add API for verification of inline signed messages 2022-05-24 21:34:50 +02:00
fd9192995f
Rename DetachInbandSignatureAndMessage to InlineDetach 2022-05-24 21:19:37 +02:00
0bfc12c1e1
Add withKeypassword() methods to Decrypt 2022-05-24 21:18:47 +02:00
0699ae740a
Add withKeyPassword() methods to Sign 2022-05-24 21:18:37 +02:00
146a2bd761
Add withKeyPassword() methods to GenerateKey 2022-05-24 21:13:30 +02:00
be2fd9de49
Add javadoc to SignAs 2022-05-24 21:07:50 +02:00
33c8e5edc5
Switch to version agnostic SOP spec URLs 2022-05-07 22:17:41 +02:00
4e74fc6aa3
SOP-Java 1.2.4-SNAPSHOT 2022-04-29 16:52:02 +02:00
417115dfe1
SOP-Java 1.2.3 2022-04-29 16:49:24 +02:00
3dbeda45f2
Update changelog 2022-04-26 00:37:24 +02:00
5c1862fe7d
Move jsr305 version to version.gradle 2022-04-26 00:34:48 +02:00
a6be316ed2
Move picocli version to version.gradle 2022-04-26 00:34:29 +02:00
aa346459e2
Bump mockito to 4.5.1 2022-04-26 00:33:20 +02:00
8753480e82
Move junit5-system-exit version to version.gradle 2022-04-26 00:32:03 +02:00
0065401c15
Update README 2022-04-11 14:50:28 +02:00
807689d516
SOP-Java 1.2.3-SNAPSHOT 2022-04-11 14:12:44 +02:00
ea9116cec5
SOP-Java 1.2.2 2022-04-11 14:10:43 +02:00
5178167214
Update CHANGELOG 2022-04-11 14:09:30 +02:00
eb80c14ce3
Add command name and description for parent command 2022-04-11 13:59:09 +02:00
69b9881798
Move junit version to version.gradle 2022-04-11 12:43:22 +02:00
ff0506d29c
SOP-Java 1.2.2-SNAPSHOT 2022-04-11 12:40:56 +02:00
bdd18c1a11
SOP-Java 1.2.1 2022-04-11 12:38:17 +02:00
e154b3e535
Add missing javadoc 2022-04-11 12:35:56 +02:00
cec2ad8c1e
Update changelog 2022-04-11 12:17:36 +02:00
1030de2ad3
CLI: Document exit codes 2022-04-11 12:13:13 +02:00
06cea1235a
Make sop-java-picocli:jar depend on sop-java:jar 2022-04-11 12:12:57 +02:00
4b6f00e40e
Set mainClass name in application section 2022-04-11 12:12:19 +02:00
866bc92d07
Update changelog 2022-03-11 17:23:56 +01:00
6c392addc1
Add hidden generate-completion command
This command can be used to generate zsh/bash autocompletion.
See https://picocli.info/autocomplete.html
2022-03-11 17:22:52 +01:00
cb16c1ce84
Update CHANGELOG 2022-02-21 15:22:58 +01:00
bbd5b75427
Bump dependency info.picocli:picocli to 4.6.3 2022-02-21 15:20:17 +01:00
582570166c
Bump dependency org.mockito:mockito-core to 4.3.1 2022-02-21 15:19:50 +01:00
e6d6f0337f
Bump dependency com.ginsberg:junit5-system-exit to 1.1.2 2022-02-21 15:19:14 +01:00
4177780653
Host javadoc on javadoc.io 2022-02-11 14:27:29 +01:00
780abbbc51
Add TODO for allowing non-utf8 passwords during decryption 2022-02-10 11:35:22 +01:00
e88d9f9dab
SOP-Java 1.2.1-SNAPSHOT 2022-02-10 00:09:56 +01:00
3b316bd06b
SOP-Java 1.2.0 2022-02-10 00:07:34 +01:00
25915d1204
Merge pull request #3 from pgpainless/indirectArgs
Treat password and sessionkkey arguments as indirect data types
2022-02-10 00:04:29 +01:00
8877bae675
Implement detection of non-UTF8 passwords 2022-02-09 23:59:45 +01:00
3e1502ff2a
Fix exception handling and add tests 2022-02-09 23:35:44 +01:00
117117b735
Treat password and sessionkkey arguments as indirect data types 2022-02-09 15:12:23 +01:00
a2f2069380
Move and update project badges 2022-01-15 14:55:02 +01:00
c2637c38eb
Add .travis.yml 2022-01-15 14:49:43 +01:00
7bb06939e7
Restore reuse compliance 2022-01-15 14:45:13 +01:00
9950668792
Switch sop-java from 'java' to 'java-library' plugin 2022-01-12 01:26:31 +01:00
7db949d79b
Add LICENSE 2022-01-11 15:25:59 +01:00
b2cca38ba7 Fix dependencies 2022-01-11 15:09:06 +01:00
1b24da7f1e Fix root project name 2022-01-11 15:09:06 +01:00
f4e882ce8b SOP-Java 1.1.1-SNAPSHOT 2022-01-11 15:09:06 +01:00
269 changed files with 14104 additions and 3799 deletions

29
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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
View 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
View 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

View file

@ -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
View 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
View 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
View 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.

View file

@ -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
[![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java)
[![Spec Revision: 14](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/14/)
[![Coverage Status](https://coveralls.io/repos/github/pgpainless/sop-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/sop-java?branch=main)
[![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/sop-java)](https://api.reuse.software/info/github.com/pgpainless/sop-java)
The [Stateless OpenPGP Protocol](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) specification
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.
[![Packaging status](https://repology.org/badge/vertical-allrepos/sop-java.svg)](https://repology.org/project/pgpainless/versions)
[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/sop-java)](https://search.maven.org/artifact/org.pgpainless/sop-java)
`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
View file

@ -0,0 +1,32 @@
# SPDX-FileCopyrightText: 2025 Paul Schaub <info@pgpainless.org>
#
# SPDX-License-Identifier: CC0-1.0
version = 1
SPDX-PackageName = "SOP-Java"
SPDX-PackageSupplier = "Paul Schaub <info@pgpainless.org>"
SPDX-PackageDownloadLocation = "https://pgpainless.org"
[[annotations]]
path = "gradle**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2015 the original author or authors."
SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
path = ".woodpecker/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2022 the original author or authors."
SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
path = "external-sop/src/main/resources/sop/testsuite/external/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2023 the original author or authors"
SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
path = ".github/ISSUE_TEMPLATE/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2024 the original author or authors"
SPDX-License-Identifier = "Apache-2.0"

View file

@ -18,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
View 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
View 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
}

View 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() }
}
}
}

View 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() }
}
}
}

View 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)
}

View file

@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.InputStream
import java.util.*
import sop.Ready
import sop.external.ExternalSOP
import sop.operation.CertifyUserId
class CertifyUserIdExternal(binary: String, environment: Properties) : CertifyUserId {
private val commandList = mutableListOf(binary, "certify-userid")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var argCount = 0
private val keys: MutableList<String> = mutableListOf()
override fun noArmor(): CertifyUserId = apply { commandList.add("--no-armor") }
override fun userId(userId: String): CertifyUserId = apply {
commandList.add("--userid")
commandList.add(userId)
}
override fun withKeyPassword(password: ByteArray): CertifyUserId = apply {
commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount")
envList.add("KEY_PASSWORD_$argCount=${String(password)}")
argCount += 1
}
override fun noRequireSelfSig(): CertifyUserId = apply {
commandList.add("--no-require-self-sig")
}
override fun keys(keys: InputStream): CertifyUserId = apply {
this.keys.add("@ENV:KEY_$argCount")
envList.add("KEY_$argCount=${ExternalSOP.readString(keys)}")
argCount += 1
}
override fun certs(certs: InputStream): Ready =
ExternalSOP.executeTransformingOperation(
Runtime.getRuntime(), commandList.plus("--").plus(keys), envList, certs)
}

View file

@ -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)
}

View 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)
}

View 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)
}
}
}

View 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
}
}

View 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)
}
}
}

View 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)
}
}
}

View 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)
}

View 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)
}

View 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)
}
}
}

View 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
}
}

View 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
}
}

View 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) }
}
}

View file

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.InputStream
import java.util.*
import sop.Ready
import sop.external.ExternalSOP
import sop.operation.MergeCerts
class MergeCertsExternal(binary: String, environment: Properties) : MergeCerts {
private val commandList = mutableListOf(binary, "merge-certs")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var argCount = 0
override fun noArmor(): MergeCerts = apply { commandList.add("--no-armor") }
override fun updates(updateCerts: InputStream): MergeCerts = apply {
commandList.add("@ENV:CERT_$argCount")
envList.add("CERT_$argCount=${ExternalSOP.readString(updateCerts)}")
argCount += 1
}
override fun baseCertificates(certs: InputStream): Ready =
ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, certs)
}

View file

@ -0,0 +1,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)
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.InputStream
import java.util.*
import sop.Ready
import sop.external.ExternalSOP
import sop.operation.UpdateKey
class UpdateKeyExternal(binary: String, environment: Properties) : UpdateKey {
private val commandList = mutableListOf(binary, "update-key")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var argCount = 0
override fun noArmor(): UpdateKey = apply { commandList.add("--no-armor") }
override fun signingOnly(): UpdateKey = apply { commandList.add("--signing-only") }
override fun noAddedCapabilities(): UpdateKey = apply {
commandList.add("--no-added-capabilities")
}
override fun withKeyPassword(password: ByteArray): UpdateKey = apply {
commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount")
envList.add("KEY_PASSWORD_$argCount=${String(password)}")
argCount += 1
}
override fun mergeCerts(certs: InputStream): UpdateKey = apply {
commandList.add("--merge-certs")
commandList.add("@ENV:CERT_$argCount")
envList.add("CERT_$argCount=${ExternalSOP.readString(certs)}")
argCount += 1
}
override fun key(key: InputStream): Ready =
ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, key)
}

View file

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation
import java.io.InputStream
import java.util.*
import sop.external.ExternalSOP
import sop.operation.ValidateUserId
import sop.util.UTCUtil
class ValidateUserIdExternal(binary: String, environment: Properties) : ValidateUserId {
private val commandList = mutableListOf(binary, "validate-userid")
private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList()
private var argCount = 0
private var userId: String? = null
private val authorities: MutableList<String> = mutableListOf()
override fun addrSpecOnly(): ValidateUserId = apply { commandList.add("--addr-spec-only") }
override fun userId(userId: String): ValidateUserId = apply { this.userId = userId }
override fun authorities(certs: InputStream): ValidateUserId = apply {
this.authorities.add("@ENV:CERT_$argCount")
envList.add("CERT_$argCount=${ExternalSOP.readString(certs)}")
argCount += 1
}
override fun subjects(certs: InputStream): Boolean {
ExternalSOP.executeTransformingOperation(
Runtime.getRuntime(), commandList.plus(userId!!).plus(authorities), envList, certs)
.bytes
return true
}
override fun validateAt(date: Date): ValidateUserId = apply {
commandList.add("--validate-at=${UTCUtil.formatUTCDate(date)}")
}
}

View file

@ -0,0 +1,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)
}
}
}

View file

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

View file

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

View 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"
}
]
}
]
}

View 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;
}
}

View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external;
import org.junit.platform.suite.api.IncludeClassNamePatterns;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SuiteDisplayName;
@Suite
@SuiteDisplayName("External SOP Tests")
@SelectPackages("sop.testsuite.operation")
@IncludeClassNamePatterns(".*Test")
public class ExternalTestSuite {
}

View file

@ -1,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
View file

@ -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" "$@"

View file

@ -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'

View file

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

View file

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
plugins {
id 'java-library'
}
group 'org.pgpainless'
repositories {
mavenCentral()
}
dependencies {
implementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
implementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
runtimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
implementation project(":sop-java")
api "org.slf4j:slf4j-api:$slf4jVersion"
testImplementation "ch.qos.logback:logback-classic:$logbackVersion"
// @Nonnull, @Nullable...
implementation "com.google.code.findbugs:jsr305:$jsrVersion"
api "com.google.code.gson:gson:$gsonVersion"
}

View file

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken
import java.text.ParseException
class GsonParser(
private val gson: Gson = Gson()
) : Verification.JSONParser {
override fun parse(string: String): Verification.JSON {
try {
return gson.fromJson(string, object : TypeToken<Verification.JSON>(){}.type)
} catch (e: JsonSyntaxException) {
throw ParseException(e.message, 0)
}
}
}

View file

@ -0,0 +1,16 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import com.google.gson.Gson
class GsonSerializer(
private val gson: Gson = Gson()
) : Verification.JSONSerializer {
override fun serialize(json: Verification.JSON): String {
return gson.toJson(json)
}
}

View file

@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.text.ParseException
class GsonSerializerAndParserTest {
private val serializer: GsonSerializer = GsonSerializer()
private val parser: GsonParser = GsonParser()
@Test
fun simpleSingleTest() {
val before = Verification.JSON("/tmp/alice.pgp")
val json = serializer.serialize(before)
assertEquals("{\"signers\":[\"/tmp/alice.pgp\"]}", json)
val after = parser.parse(json)
assertEquals(before, after)
}
@Test
fun simpleListTest() {
val before = Verification.JSON(listOf("/tmp/alice.pgp", "/tmp/bob.asc"))
val json = serializer.serialize(before)
assertEquals("{\"signers\":[\"/tmp/alice.pgp\",\"/tmp/bob.asc\"]}", json)
val after = parser.parse(json)
assertEquals(before, after)
}
@Test
fun withCommentTest() {
val before = Verification.JSON(
listOf("/tmp/alice.pgp"),
"This is a comment.",
null)
val json = serializer.serialize(before)
assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\"}", json)
val after = parser.parse(json)
assertEquals(before, after)
}
@Test
fun withExtStringTest() {
val before = Verification.JSON(
listOf("/tmp/alice.pgp"),
"This is a comment.",
"This is an ext object string.")
val json = serializer.serialize(before)
assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\",\"ext\":\"This is an ext object string.\"}", json)
val after = parser.parse(json)
assertEquals(before, after)
}
@Test
fun withExtListTest() {
val before = Verification.JSON(
listOf("/tmp/alice.pgp"),
"This is a comment.",
listOf(1.0,2.0,3.0))
val json = serializer.serialize(before)
assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\",\"ext\":[1.0,2.0,3.0]}", json)
val after = parser.parse(json)
assertEquals(before, after)
}
@Test
fun parseInvalidJSON() {
assertThrows<ParseException> { parser.parse("Invalid") }
}
@Test
fun parseMalformedJSON() {
// Missing '}'
assertThrows<ParseException> { parser.parse("{\"signers\":[\"Alice\"]") }
}
}

View file

@ -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.
[![javadoc](https://javadoc.io/badge2/org.pgpainless/sop-java-picocli/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/sop-java-picocli)
[![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/sop-java-picocli)](https://search.maven.org/artifact/org.pgpainless/sop-java-picocli)
Implementation of the [Stateless OpenPGP Command Line Interface](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) specification.
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!

View file

@ -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']
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View 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
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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());
}
}
}

View file

@ -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());
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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
}
}

View file

@ -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
}
}

View 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
}
}

View 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
}
}

View file

@ -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())
}
}
}
}

View file

@ -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)
}
}
}

View file

@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import picocli.CommandLine.Parameters
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException.BadData
import sop.exception.SOPGPException.UnsupportedOption
@Command(
name = "certify-userid",
resourceBundle = "msg_certify-userid",
exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE,
showEndOfOptionsDelimiterInUsageHelp = true)
class CertifyUserIdCmd : AbstractSopCmd() {
@Option(names = ["--no-armor"], negatable = true) var armor = true
@Option(names = ["--userid"], required = true, arity = "1..*", paramLabel = "USERID")
var userIds: List<String> = listOf()
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
var withKeyPassword: List<String> = listOf()
@Option(names = ["--no-require-self-sig"]) var noRequireSelfSig = false
@Parameters(paramLabel = "KEYS", arity = "1..*") var keys: List<String> = listOf()
override fun run() {
val certifyUserId =
throwIfUnsupportedSubcommand(SopCLI.getSop().certifyUserId(), "certify-userid")
if (!armor) {
certifyUserId.noArmor()
}
if (noRequireSelfSig) {
certifyUserId.noRequireSelfSig()
}
for (userId in userIds) {
certifyUserId.userId(userId)
}
for (passwordFileName in withKeyPassword) {
try {
val password = stringFromInputStream(getInput(passwordFileName))
certifyUserId.withKeyPassword(password)
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg =
getMsg("sop.error.feature_support.option_not_supported", "--with-key-password")
throw UnsupportedOption(errorMsg, unsupportedOption)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
for (keyInput in keys) {
try {
getInput(keyInput).use { certifyUserId.keys(it) }
} catch (e: IOException) {
throw RuntimeException(e)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput)
throw BadData(errorMsg, badData)
}
}
try {
val ready = certifyUserId.certs(System.`in`)
ready.writeTo(System.out)
} catch (e: IOException) {
throw RuntimeException(e)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_private_key", "STDIN")
throw BadData(errorMsg, badData)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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"
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}
}

View file

@ -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)
}
}
}

View file

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import picocli.CommandLine
import picocli.CommandLine.Command
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException
@Command(
name = "merge-certs",
resourceBundle = "msg_merge-certs",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
class MergeCertsCmd : AbstractSopCmd() {
@CommandLine.Option(names = ["--no-armor"], negatable = true) var armor = true
@CommandLine.Parameters(paramLabel = "CERTS") var updates: List<String> = listOf()
override fun run() {
val mergeCerts = throwIfUnsupportedSubcommand(SopCLI.getSop().mergeCerts(), "merge-certs")
if (!armor) {
mergeCerts.noArmor()
}
for (certFileName in updates) {
try {
getInput(certFileName).use { mergeCerts.updates(it) }
} catch (e: IOException) {
throw RuntimeException(e)
}
}
try {
val ready = mergeCerts.baseCertificates(System.`in`)
ready.writeTo(System.out)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException.*
@Command(
name = "update-key",
resourceBundle = "msg_update-key",
exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE)
class UpdateKeyCmd : AbstractSopCmd() {
@Option(names = ["--no-armor"], negatable = true) var armor = true
@Option(names = ["--signing-only"]) var signingOnly = false
@Option(names = ["--no-added-capabilities"]) var noAddedCapabilities = false
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD")
var withKeyPassword: List<String> = listOf()
@Option(names = ["--merge-certs"], paramLabel = "CERTS") var mergeCerts: List<String> = listOf()
override fun run() {
val updateKey = throwIfUnsupportedSubcommand(SopCLI.getSop().updateKey(), "update-key")
if (!armor) {
updateKey.noArmor()
}
if (signingOnly) {
updateKey.signingOnly()
}
if (noAddedCapabilities) {
updateKey.noAddedCapabilities()
}
for (passwordFileName in withKeyPassword) {
try {
val password = stringFromInputStream(getInput(passwordFileName))
updateKey.withKeyPassword(password)
} catch (unsupportedOption: UnsupportedOption) {
val errorMsg =
getMsg("sop.error.feature_support.option_not_supported", "--with-key-password")
throw UnsupportedOption(errorMsg, unsupportedOption)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
for (certInput in mergeCerts) {
try {
getInput(certInput).use { updateKey.mergeCerts(it) }
} catch (e: IOException) {
throw RuntimeException(e)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput)
throw BadData(errorMsg, badData)
}
}
try {
val ready = updateKey.key(System.`in`)
ready.writeTo(System.out)
} catch (e: IOException) {
throw RuntimeException(e)
} catch (badData: BadData) {
val errorMsg = getMsg("sop.error.input.not_a_private_key", "STDIN")
throw BadData(errorMsg, badData)
}
}
}

View file

@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands
import java.io.IOException
import java.util.*
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import picocli.CommandLine.Parameters
import sop.cli.picocli.SopCLI
import sop.exception.SOPGPException
import sop.util.HexUtil.Companion.bytesToHex
@Command(
name = "validate-userid",
resourceBundle = "msg_validate-userid",
exitCodeOnInvalidInput = SOPGPException.MissingArg.EXIT_CODE,
showEndOfOptionsDelimiterInUsageHelp = true)
class ValidateUserIdCmd : AbstractSopCmd() {
@Option(names = ["--addr-spec-only"]) var addrSpecOnly: Boolean = false
@Option(names = ["--validate-at"]) var validateAt: Date? = null
@Parameters(index = "0", arity = "1", paramLabel = "USERID") lateinit var userId: String
@Parameters(index = "1..*", arity = "1..*", paramLabel = "CERTS")
var authorities: List<String> = listOf()
override fun run() {
val validateUserId =
throwIfUnsupportedSubcommand(SopCLI.getSop().validateUserId(), "validate-userid")
if (addrSpecOnly) {
validateUserId.addrSpecOnly()
}
if (validateAt != null) {
validateUserId.validateAt(validateAt!!)
}
validateUserId.userId(userId)
for (authority in authorities) {
try {
getInput(authority).use { validateUserId.authorities(it) }
} catch (e: IOException) {
throw RuntimeException(e)
} catch (b: SOPGPException.BadData) {
val errorMsg = getMsg("sop.error.input.not_a_certificate", authority)
throw SOPGPException.BadData(errorMsg, b)
}
}
try {
val valid = validateUserId.subjects(System.`in`)
if (!valid) {
val errorMsg = getMsg("sop.error.runtime.any_cert_user_id_no_match", userId)
throw SOPGPException.CertUserIdNoMatch(errorMsg)
}
} catch (e: SOPGPException.CertUserIdNoMatch) {
val errorMsg =
if (e.fingerprint != null) {
getMsg(
"sop.error.runtime.cert_user_id_no_match",
bytesToHex(e.fingerprint!!),
userId)
} else {
getMsg("sop.error.runtime.any_cert_user_id_no_match", userId)
}
throw SOPGPException.CertUserIdNoMatch(errorMsg, e)
} catch (e: SOPGPException.BadData) {
val errorMsg = getMsg("sop.error.input.not_a_certificate", "STDIN")
throw SOPGPException.BadData(errorMsg, e)
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}

View file

@ -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())
}
}
}

View file

@ -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
}
}
}

View 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

View 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

View file

@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Certify OpenPGP Certificate User IDs
no-armor=ASCII armor the output
userid=Identities that shall be certified
with-key-password.0=Passphrase to unlock the secret key(s).
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
no-require-self-sig=Certify the UserID regardless of whether self-certifications are present
KEYS[0..*]=Private keys
standardInput=CERTS
standardInputDescription=Certificates that shall be certified
standardOutput=CERTS
standardOutputDescription=Certified certificates
picocli.endofoptions.description=End of options. Remainder are positional parameters. Fixes 'Missing required parameter' error
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Zertifiziere OpenPGP Zertifikat Identitäten
no-armor=Schütze Ausgabe mit ASCII Armor
userid=Identität, die zertifiziert werden soll
with-key-password.0=Passwort zum Entsperren der privaten Schlüssel
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
no-require-self-sig=Zertifiziere die Identität, unabhängig davon, ob eine Selbstzertifizierung vorhanden ist
KEYS[0..*]=Private Schlüssel
standardInputDescription=Zertifikate, auf denen Identitäten zertifiziert werden sollen
standardOutputDescription=Zertifizierte Zertifikate
picocli.endofoptions.description=Ende der Optionen. Der Rest sind Positionsparameter. Behebt 'Missing required parameter' Fehler
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View 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

View file

@ -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