Compare commits

...

240 commits
7.0.2 ... 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
290 changed files with 9118 additions and 7598 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. -->

View file

@ -1,24 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: SOP-Java
Upstream-Contact: Paul Schaub <info@pgpainless.org>
Source: https://pgpainless.org
# Sample paragraph, commented out:
#
# Files: src/*
# Copyright: $YEAR $NAME <$CONTACT>
# License: ...
# Gradle build tool
Files: gradle*
Copyright: 2015 the original author or authors.
License: Apache-2.0
# Woodpecker build files
Files: .woodpecker/*
Copyright: 2022 the original author or authors.
License: Apache-2.0
Files: external-sop/src/main/resources/sop/testsuite/external/*
Copyright: 2023 the original author or authors
License: Apache-2.0

View file

@ -1,6 +1,8 @@
pipeline:
steps:
run:
image: gradle:7.5-jdk8-jammy
when:
event: push
image: gradle:7.6-jdk11-jammy
commands:
# Install Sequoia-SOP
- apt update && apt install --yes sqop
@ -14,4 +16,6 @@ pipeline:
- gradle check javadocAll
# Code has coverage
- gradle jacocoRootReport coveralls
secrets: [COVERALLS_REPO_TOKEN]
environment:
COVERALLS_REPO_TOKEN:
from_secret: coveralls_repo_token

View file

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

View file

@ -6,6 +6,95 @@ 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

View file

@ -7,7 +7,7 @@ SPDX-License-Identifier: Apache-2.0
# SOP for Java
[![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java)
[![Spec Revision: 7](https://img.shields.io/badge/Spec%20Revision-7-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/07/)
[![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)
@ -25,6 +25,10 @@ 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!)
@ -33,9 +37,11 @@ compatible with the SOP-CLI specification.
|-------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
| [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,6 +63,20 @@ 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 {
@ -94,7 +104,7 @@ allprojects {
}
jacoco {
toolVersion = "0.8.7"
toolVersion = "0.8.8"
}
jacocoTestReport {
@ -102,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
}
}
@ -120,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
}
@ -225,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
@ -236,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')

View file

@ -15,7 +15,9 @@ repositories {
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
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"
@ -27,7 +29,7 @@ dependencies {
// The ExternalTestSubjectFactory reads json config file to find configured SOP binaries...
testImplementation "com.google.code.gson:gson:$gsonVersion"
// ...and extends TestSubjectFactory
testImplementation(testFixtures(project(":sop-java")))
testImplementation(project(":sop-java-testfixtures"))
}
test {

View file

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

View file

@ -1,42 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation;
import sop.Ready;
import sop.enums.ArmorLabel;
import sop.exception.SOPGPException;
import sop.external.ExternalSOP;
import sop.operation.Armor;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* Implementation of the {@link Armor} operation using an external SOP binary.
*/
public class ArmorExternal implements Armor {
private final List<String> commandList = new ArrayList<>();
private final List<String> envList;
public ArmorExternal(String binary, Properties environment) {
commandList.add(binary);
commandList.add("armor");
envList = ExternalSOP.propertiesToEnv(environment);
}
@Override
public Armor label(ArmorLabel label) throws SOPGPException.UnsupportedOption {
commandList.add("--label=" + label);
return this;
}
@Override
public Ready data(InputStream data) throws SOPGPException.BadData {
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data);
}
}

View file

@ -1,57 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.external.ExternalSOP;
import sop.operation.ChangeKeyPassword;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
public class ChangeKeyPasswordExternal implements ChangeKeyPassword {
private final List<String> commandList = new ArrayList<>();
private final List<String> envList;
private int keyPasswordCounter = 0;
public ChangeKeyPasswordExternal(String binary, Properties environment) {
this.commandList.add(binary);
this.commandList.add("decrypt");
this.envList = ExternalSOP.propertiesToEnv(environment);
}
@Override
public ChangeKeyPassword noArmor() {
this.commandList.add("--no-armor");
return this;
}
@Override
public ChangeKeyPassword oldKeyPassphrase(String oldPassphrase) {
this.commandList.add("--old-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter);
this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + oldPassphrase);
keyPasswordCounter++;
return this;
}
@Override
public ChangeKeyPassword newKeyPassphrase(String newPassphrase) {
this.commandList.add("--new-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter);
this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + newPassphrase);
keyPasswordCounter++;
return this;
}
@Override
public Ready keys(InputStream inputStream) throws SOPGPException.KeyIsProtected, SOPGPException.BadData {
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, inputStream);
}
}

View file

@ -1,35 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.external.ExternalSOP;
import sop.operation.Dearmor;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* Implementation of the {@link Dearmor} operation using an external SOP binary.
*/
public class DearmorExternal implements Dearmor {
private final List<String> commandList = new ArrayList<>();
private final List<String> envList;
public DearmorExternal(String binary, Properties environment) {
commandList.add(binary);
commandList.add("dearmor");
envList = ExternalSOP.propertiesToEnv(environment);
}
@Override
public Ready data(InputStream data) throws SOPGPException.BadData {
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data);
}
}

View file

@ -1,176 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation;
import sop.DecryptionResult;
import sop.ReadyWithResult;
import sop.SessionKey;
import sop.Verification;
import sop.exception.SOPGPException;
import sop.external.ExternalSOP;
import sop.operation.Decrypt;
import sop.util.UTCUtil;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
/**
* Implementation of the {@link Decrypt} operation using an external SOP binary.
*/
public class DecryptExternal implements Decrypt {
private final ExternalSOP.TempDirProvider tempDirProvider;
private final List<String> commandList = new ArrayList<>();
private final List<String> envList;
private int verifyWithCounter = 0;
private int withSessionKeyCounter = 0;
private int withPasswordCounter = 0;
private int keyCounter = 0;
private int withKeyPasswordCounter = 0;
public DecryptExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) {
this.tempDirProvider = tempDirProvider;
this.commandList.add(binary);
this.commandList.add("decrypt");
this.envList = ExternalSOP.propertiesToEnv(environment);
}
@Override
public Decrypt verifyNotBefore(Date timestamp)
throws SOPGPException.UnsupportedOption {
this.commandList.add("--verify-not-before=" + UTCUtil.formatUTCDate(timestamp));
return this;
}
@Override
public Decrypt verifyNotAfter(Date timestamp)
throws SOPGPException.UnsupportedOption {
this.commandList.add("--verify-not-after=" + UTCUtil.formatUTCDate(timestamp));
return this;
}
@Override
public Decrypt verifyWithCert(InputStream cert)
throws SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException {
String envVar = "VERIFY_WITH_" + verifyWithCounter++;
commandList.add("--verify-with=@ENV:" + envVar);
envList.add(envVar + "=" + ExternalSOP.readString(cert));
return this;
}
@Override
public Decrypt withSessionKey(SessionKey sessionKey)
throws SOPGPException.UnsupportedOption {
String envVar = "SESSION_KEY_" + withSessionKeyCounter++;
commandList.add("--with-session-key=@ENV:" + envVar);
envList.add(envVar + "=" + sessionKey);
return this;
}
@Override
public Decrypt withPassword(String password)
throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
String envVar = "PASSWORD_" + withPasswordCounter++;
commandList.add("--with-password=@ENV:" + envVar);
envList.add(envVar + "=" + password);
return this;
}
@Override
public Decrypt withKey(InputStream key)
throws SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException {
String envVar = "KEY_" + keyCounter++;
commandList.add("@ENV:" + envVar);
envList.add(envVar + "=" + ExternalSOP.readString(key));
return this;
}
@Override
public Decrypt withKeyPassword(byte[] password)
throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable {
String envVar = "KEY_PASSWORD_" + withKeyPasswordCounter++;
commandList.add("--with-key-password=@ENV:" + envVar);
envList.add(envVar + "=" + new String(password));
return this;
}
@Override
public ReadyWithResult<DecryptionResult> ciphertext(InputStream ciphertext)
throws SOPGPException.BadData, SOPGPException.MissingArg, SOPGPException.CannotDecrypt,
SOPGPException.KeyIsProtected, IOException {
File tempDir = tempDirProvider.provideTempDirectory();
File sessionKeyOut = new File(tempDir, "session-key-out");
sessionKeyOut.delete();
commandList.add("--session-key-out=" + sessionKeyOut.getAbsolutePath());
File verifyOut = new File(tempDir, "verifications-out");
verifyOut.delete();
if (verifyWithCounter != 0) {
commandList.add("--verify-out=" + verifyOut.getAbsolutePath());
}
String[] command = commandList.toArray(new String[0]);
String[] env = envList.toArray(new String[0]);
try {
Process process = Runtime.getRuntime().exec(command, env);
OutputStream processOut = process.getOutputStream();
InputStream processIn = process.getInputStream();
return new ReadyWithResult<DecryptionResult>() {
@Override
public DecryptionResult writeTo(OutputStream outputStream) throws IOException {
byte[] buf = new byte[4096];
int r;
while ((r = ciphertext.read(buf)) > 0) {
processOut.write(buf, 0, r);
}
ciphertext.close();
processOut.close();
while ((r = processIn.read(buf)) > 0) {
outputStream.write(buf, 0 , r);
}
processIn.close();
outputStream.close();
ExternalSOP.finish(process);
FileInputStream sessionKeyOutIn = new FileInputStream(sessionKeyOut);
String line = ExternalSOP.readString(sessionKeyOutIn);
SessionKey sessionKey = SessionKey.fromString(line.trim());
sessionKeyOutIn.close();
sessionKeyOut.delete();
List<Verification> verifications = new ArrayList<>();
if (verifyWithCounter != 0) {
FileInputStream verifyOutIn = new FileInputStream(verifyOut);
BufferedReader reader = new BufferedReader(new InputStreamReader(verifyOutIn));
while ((line = reader.readLine()) != null) {
verifications.add(Verification.fromString(line.trim()));
}
reader.close();
}
return new DecryptionResult(sessionKey, verifications);
}
};
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,136 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation;
import sop.MicAlg;
import sop.ReadyWithResult;
import sop.SigningResult;
import sop.enums.SignAs;
import sop.exception.SOPGPException;
import sop.external.ExternalSOP;
import sop.operation.DetachedSign;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* Implementation of the {@link DetachedSign} operation using an external SOP binary.
*/
public class DetachedSignExternal implements DetachedSign {
private final ExternalSOP.TempDirProvider tempDirProvider;
private final List<String> commandList = new ArrayList<>();
private final List<String> envList;
private int withKeyPasswordCounter = 0;
private int keyCounter = 0;
public DetachedSignExternal(String binary, Properties properties, ExternalSOP.TempDirProvider tempDirProvider) {
this.tempDirProvider = tempDirProvider;
commandList.add(binary);
commandList.add("sign");
envList = ExternalSOP.propertiesToEnv(properties);
}
@Override
public DetachedSign noArmor() {
commandList.add("--no-armor");
return this;
}
@Override
public DetachedSign key(InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException {
String envVar = "KEY_" + keyCounter++;
commandList.add("@ENV:" + envVar);
envList.add(envVar + "=" + ExternalSOP.readString(key));
return this;
}
@Override
public DetachedSign withKeyPassword(byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable {
String envVar = "WITH_KEY_PASSWORD_" + withKeyPasswordCounter++;
commandList.add("--with-key-password=@ENV:" + envVar);
envList.add(envVar + "=" + new String(password));
return this;
}
@Override
public DetachedSign mode(SignAs mode) throws SOPGPException.UnsupportedOption {
commandList.add("--as=" + mode);
return this;
}
@Override
public ReadyWithResult<SigningResult> data(InputStream data)
throws IOException, SOPGPException.KeyIsProtected, SOPGPException.ExpectedText {
File tempDir = tempDirProvider.provideTempDirectory();
File micAlgOut = new File(tempDir, "micAlgOut");
micAlgOut.delete();
commandList.add("--micalg-out=" + micAlgOut.getAbsolutePath());
String[] command = commandList.toArray(new String[0]);
String[] env = envList.toArray(new String[0]);
try {
Process process = Runtime.getRuntime().exec(command, env);
OutputStream processOut = process.getOutputStream();
InputStream processIn = process.getInputStream();
return new ReadyWithResult<SigningResult>() {
@Override
public SigningResult writeTo(OutputStream outputStream) throws IOException {
byte[] buf = new byte[4096];
int r;
while ((r = data.read(buf)) > 0) {
processOut.write(buf, 0, r);
}
data.close();
try {
processOut.close();
} catch (IOException e) {
// Ignore Stream closed
if (!"Stream closed".equals(e.getMessage())) {
throw e;
}
}
while ((r = processIn.read(buf)) > 0) {
outputStream.write(buf, 0 , r);
}
processIn.close();
outputStream.close();
ExternalSOP.finish(process);
SigningResult.Builder builder = SigningResult.builder();
if (micAlgOut.exists()) {
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(micAlgOut)));
String line = reader.readLine();
if (line != null && !line.trim().isEmpty()) {
MicAlg micAlg = new MicAlg(line.trim());
builder.setMicAlg(micAlg);
}
reader.close();
micAlgOut.delete();
}
return builder.build();
}
};
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,111 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation;
import sop.Verification;
import sop.exception.SOPGPException;
import sop.external.ExternalSOP;
import sop.operation.DetachedVerify;
import sop.operation.VerifySignatures;
import sop.util.UTCUtil;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
/**
* Implementation of the {@link DetachedVerify} operation using an external SOP binary.
*/
public class DetachedVerifyExternal implements DetachedVerify {
private final List<String> commandList = new ArrayList<>();
private final List<String> envList;
private final Set<InputStream> certs = new HashSet<>();
private InputStream signatures;
private int certCounter = 0;
public DetachedVerifyExternal(String binary, Properties environment) {
commandList.add(binary);
commandList.add("verify");
envList = ExternalSOP.propertiesToEnv(environment);
}
@Override
public DetachedVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption {
commandList.add("--not-before=" + UTCUtil.formatUTCDate(timestamp));
return this;
}
@Override
public DetachedVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption {
commandList.add("--not-after=" + UTCUtil.formatUTCDate(timestamp));
return this;
}
@Override
public DetachedVerify cert(InputStream cert) throws SOPGPException.BadData {
this.certs.add(cert);
return this;
}
@Override
public VerifySignatures signatures(InputStream signatures) throws SOPGPException.BadData {
this.signatures = signatures;
return this;
}
@Override
public List<Verification> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData {
commandList.add("@ENV:SIGNATURE");
envList.add("SIGNATURE=" + ExternalSOP.readString(signatures));
for (InputStream cert : certs) {
String envVar = "CERT_" + certCounter++;
commandList.add("@ENV:" + envVar);
envList.add(envVar + "=" + ExternalSOP.readString(cert));
}
String[] command = commandList.toArray(new String[0]);
String[] env = envList.toArray(new String[0]);
try {
Process process = Runtime.getRuntime().exec(command, env);
OutputStream processOut = process.getOutputStream();
InputStream processIn = process.getInputStream();
byte[] buf = new byte[4096];
int r;
while ((r = data.read(buf)) > 0) {
processOut.write(buf, 0, r);
}
data.close();
processOut.close();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(processIn));
List<Verification> verifications = new ArrayList<>();
String line = null;
while ((line = bufferedReader.readLine()) != null) {
verifications.add(Verification.fromString(line));
}
ExternalSOP.finish(process);
return verifications;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,99 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation;
import sop.Ready;
import sop.enums.EncryptAs;
import sop.exception.SOPGPException;
import sop.external.ExternalSOP;
import sop.operation.Encrypt;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* Implementation of the {@link Encrypt} operation using an external SOP binary.
*/
public class EncryptExternal implements Encrypt {
private final List<String> commandList = new ArrayList<>();
private final List<String> envList;
private int SIGN_WITH_COUNTER = 0;
private int KEY_PASSWORD_COUNTER = 0;
private int PASSWORD_COUNTER = 0;
private int CERT_COUNTER = 0;
public EncryptExternal(String binary, Properties environment) {
this.commandList.add(binary);
this.commandList.add("encrypt");
this.envList = ExternalSOP.propertiesToEnv(environment);
}
@Override
public Encrypt noArmor() {
this.commandList.add("--no-armor");
return this;
}
@Override
public Encrypt mode(EncryptAs mode)
throws SOPGPException.UnsupportedOption {
this.commandList.add("--as=" + mode);
return this;
}
@Override
public Encrypt signWith(InputStream key)
throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData,
IOException {
String envVar = "SIGN_WITH_" + SIGN_WITH_COUNTER++;
commandList.add("--sign-with=@ENV:" + envVar);
envList.add(envVar + "=" + ExternalSOP.readString(key));
return this;
}
@Override
public Encrypt withKeyPassword(byte[] password)
throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
String envVar = "KEY_PASSWORD_" + KEY_PASSWORD_COUNTER++;
commandList.add("--with-key-password=@ENV:" + envVar);
envList.add(envVar + "=" + new String(password));
return this;
}
@Override
public Encrypt withPassword(String password)
throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
String envVar = "PASSWORD_" + PASSWORD_COUNTER++;
commandList.add("--with-password=@ENV:" + envVar);
envList.add(envVar + "=" + password);
return this;
}
@Override
public Encrypt withCert(InputStream cert)
throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData,
IOException {
String envVar = "CERT_" + CERT_COUNTER++;
commandList.add("@ENV:" + envVar);
envList.add(envVar + "=" + ExternalSOP.readString(cert));
return this;
}
@Override
public Encrypt profile(String profileName) {
commandList.add("--profile=" + profileName);
return this;
}
@Override
public Ready plaintext(InputStream plaintext)
throws SOPGPException.KeyIsProtected {
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, plaintext);
}
}

View file

@ -1,41 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.external.ExternalSOP;
import sop.operation.ExtractCert;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* Implementation of the {@link ExtractCert} operation using an external SOP binary.
*/
public class ExtractCertExternal implements ExtractCert {
private final List<String> commandList = new ArrayList<>();
private final List<String> envList;
public ExtractCertExternal(String binary, Properties properties) {
this.commandList.add(binary);
this.commandList.add("extract-cert");
this.envList = ExternalSOP.propertiesToEnv(properties);
}
@Override
public ExtractCert noArmor() {
this.commandList.add("--no-armor");
return this;
}
@Override
public Ready key(InputStream keyInputStream) throws SOPGPException.BadData {
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keyInputStream);
}
}

View file

@ -1,71 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.external.ExternalSOP;
import sop.operation.GenerateKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* Implementation of the {@link GenerateKey} operation using an external SOP binary.
*/
public class GenerateKeyExternal implements GenerateKey {
private final List<String> commandList = new ArrayList<>();
private final List<String> envList;
private int keyPasswordCounter = 0;
public GenerateKeyExternal(String binary, Properties environment) {
this.commandList.add(binary);
this.commandList.add("generate-key");
this.envList = ExternalSOP.propertiesToEnv(environment);
}
@Override
public GenerateKey noArmor() {
this.commandList.add("--no-armor");
return this;
}
@Override
public GenerateKey userId(String userId) {
this.commandList.add(userId);
return this;
}
@Override
public GenerateKey withKeyPassword(String password)
throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
this.commandList.add("--with-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter);
this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + password);
keyPasswordCounter++;
return this;
}
@Override
public GenerateKey profile(String profile) {
commandList.add("--profile=" + profile);
return this;
}
@Override
public GenerateKey signingOnly() {
commandList.add("--signing-only");
return this;
}
@Override
public Ready generate()
throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo {
return ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList);
}
}

View file

@ -1,103 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation;
import sop.ReadyWithResult;
import sop.Signatures;
import sop.exception.SOPGPException;
import sop.external.ExternalSOP;
import sop.operation.InlineDetach;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* Implementation of the {@link InlineDetach} operation using an external SOP binary.
*/
public class InlineDetachExternal implements InlineDetach {
private final ExternalSOP.TempDirProvider tempDirProvider;
private final List<String> commandList = new ArrayList<>();
private final List<String> envList;
public InlineDetachExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) {
this.tempDirProvider = tempDirProvider;
commandList.add(binary);
commandList.add("inline-detach");
envList = ExternalSOP.propertiesToEnv(environment);
}
@Override
public InlineDetach noArmor() {
commandList.add("--no-armor");
return this;
}
@Override
public ReadyWithResult<Signatures> message(InputStream messageInputStream) throws IOException, SOPGPException.BadData {
File tempDir = tempDirProvider.provideTempDirectory();
File signaturesOut = new File(tempDir, "signatures");
signaturesOut.delete();
commandList.add("--signatures-out=" + signaturesOut.getAbsolutePath());
String[] command = commandList.toArray(new String[0]);
String[] env = envList.toArray(new String[0]);
try {
Process process = Runtime.getRuntime().exec(command, env);
OutputStream processOut = process.getOutputStream();
InputStream processIn = process.getInputStream();
return new ReadyWithResult<Signatures>() {
@Override
public Signatures writeTo(OutputStream outputStream) throws IOException {
byte[] buf = new byte[4096];
int r;
while ((r = messageInputStream.read(buf)) > 0) {
processOut.write(buf, 0, r);
}
messageInputStream.close();
processOut.close();
while ((r = processIn.read(buf)) > 0) {
outputStream.write(buf, 0 , r);
}
processIn.close();
outputStream.close();
ExternalSOP.finish(process);
FileInputStream signaturesOutIn = new FileInputStream(signaturesOut);
ByteArrayOutputStream signaturesBuffer = new ByteArrayOutputStream();
while ((r = signaturesOutIn.read(buf)) > 0) {
signaturesBuffer.write(buf, 0, r);
}
signaturesOutIn.close();
signaturesOut.delete();
final byte[] sigBytes = signaturesBuffer.toByteArray();
return new Signatures() {
@Override
public void writeTo(OutputStream signatureOutputStream) throws IOException {
signatureOutputStream.write(sigBytes);
}
};
}
};
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,68 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation;
import sop.Ready;
import sop.enums.InlineSignAs;
import sop.exception.SOPGPException;
import sop.external.ExternalSOP;
import sop.operation.InlineSign;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* Implementation of the {@link InlineSign} operation using an external SOP binary.
*/
public class InlineSignExternal implements InlineSign {
private final List<String> commandList = new ArrayList<>();
private final List<String> envList;
private int keyCounter = 0;
private int withKeyPasswordCounter = 0;
public InlineSignExternal(String binary, Properties environment) {
commandList.add(binary);
commandList.add("inline-sign");
envList = ExternalSOP.propertiesToEnv(environment);
}
@Override
public InlineSign noArmor() {
commandList.add("--no-armor");
return this;
}
@Override
public InlineSign key(InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException {
String envVar = "KEY_" + keyCounter++;
commandList.add("@ENV:" + envVar);
envList.add(envVar + "=" + ExternalSOP.readString(key));
return this;
}
@Override
public InlineSign withKeyPassword(byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable {
String envVar = "WITH_KEY_PASSWORD_" + withKeyPasswordCounter++;
commandList.add("--with-key-password=@ENV:" + envVar);
envList.add(envVar + "=" + new String(password));
return this;
}
@Override
public InlineSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption {
commandList.add("--as=" + mode);
return this;
}
@Override
public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText {
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data);
}
}

View file

@ -1,117 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation;
import sop.ReadyWithResult;
import sop.Verification;
import sop.exception.SOPGPException;
import sop.external.ExternalSOP;
import sop.operation.InlineVerify;
import sop.util.UTCUtil;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
/**
* Implementation of the {@link InlineVerify} operation using an external SOP binary.
*/
public class InlineVerifyExternal implements InlineVerify {
private final ExternalSOP.TempDirProvider tempDirProvider;
private final List<String> commandList = new ArrayList<>();
private final List<String> envList;
private int certCounter = 0;
public InlineVerifyExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) {
this.tempDirProvider = tempDirProvider;
commandList.add(binary);
commandList.add("inline-verify");
envList = ExternalSOP.propertiesToEnv(environment);
}
@Override
public InlineVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption {
commandList.add("--not-before=" + UTCUtil.formatUTCDate(timestamp));
return this;
}
@Override
public InlineVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption {
commandList.add("--not-after=" + UTCUtil.formatUTCDate(timestamp));
return this;
}
@Override
public InlineVerify cert(InputStream cert) throws SOPGPException.BadData, IOException {
String envVar = "CERT_" + certCounter++;
commandList.add("@ENV:" + envVar);
envList.add(envVar + "=" + ExternalSOP.readString(cert));
return this;
}
@Override
public ReadyWithResult<List<Verification>> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData {
File tempDir = tempDirProvider.provideTempDirectory();
File verificationsOut = new File(tempDir, "verifications-out");
verificationsOut.delete();
commandList.add("--verifications-out=" + verificationsOut.getAbsolutePath());
String[] command = commandList.toArray(new String[0]);
String[] env = envList.toArray(new String[0]);
try {
Process process = Runtime.getRuntime().exec(command, env);
OutputStream processOut = process.getOutputStream();
InputStream processIn = process.getInputStream();
return new ReadyWithResult<List<Verification>>() {
@Override
public List<Verification> writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature {
byte[] buf = new byte[4096];
int r;
while ((r = data.read(buf)) > 0) {
processOut.write(buf, 0, r);
}
data.close();
processOut.close();
while ((r = processIn.read(buf)) > 0) {
outputStream.write(buf, 0 , r);
}
processIn.close();
outputStream.close();
ExternalSOP.finish(process);
FileInputStream verificationsOutIn = new FileInputStream(verificationsOut);
BufferedReader reader = new BufferedReader(new InputStreamReader(verificationsOutIn));
List<Verification> verificationList = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
verificationList.add(Verification.fromString(line.trim()));
}
return verificationList;
}
};
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,48 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation;
import sop.Profile;
import sop.external.ExternalSOP;
import sop.operation.ListProfiles;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
public class ListProfilesExternal implements ListProfiles {
private final List<String> commandList = new ArrayList<>();
private final List<String> envList;
public ListProfilesExternal(String binary, Properties properties) {
this.commandList.add(binary);
this.commandList.add("list-profiles");
this.envList = ExternalSOP.propertiesToEnv(properties);
}
@Override
public List<Profile> subcommand(String command) {
commandList.add(command);
try {
String output = new String(ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList).getBytes());
return toProfiles(output);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static List<Profile> toProfiles(String output) {
List<Profile> profiles = new ArrayList<>();
for (String line : output.split("\n")) {
if (line.trim().isEmpty()) {
continue;
}
profiles.add(Profile.parse(line));
}
return profiles;
}
}

View file

@ -1,48 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation;
import sop.Ready;
import sop.exception.SOPGPException;
import sop.external.ExternalSOP;
import sop.operation.RevokeKey;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
public class RevokeKeyExternal implements RevokeKey {
private final List<String> commandList = new ArrayList<>();
private final List<String> envList;
private int withKeyPasswordCounter = 0;
public RevokeKeyExternal(String binary, Properties environment) {
this.commandList.add(binary);
this.commandList.add("revoke-key");
this.envList = ExternalSOP.propertiesToEnv(environment);
}
@Override
public RevokeKey noArmor() {
this.commandList.add("--no-armor");
return this;
}
@Override
public RevokeKey withKeyPassword(byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable {
String envVar = "KEY_PASSWORD_" + withKeyPasswordCounter++;
commandList.add("--with-key-password=@ENV:" + envVar);
envList.add(envVar + "=" + new String(password));
return this;
}
@Override
public Ready keys(InputStream keys) {
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys);
}
}

View file

@ -1,157 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external.operation;
import sop.external.ExternalSOP;
import sop.operation.Version;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Properties;
/**
* Implementation of the {@link Version} operation using an external SOP binary.
*/
public class VersionExternal implements Version {
private final Runtime runtime = Runtime.getRuntime();
private final String binary;
private final Properties environment;
public VersionExternal(String binaryName, Properties environment) {
this.binary = binaryName;
this.environment = environment;
}
@Override
public String getName() {
String[] command = new String[] {binary, "version"};
String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]);
try {
Process process = runtime.exec(command, env);
BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = stdInput.readLine().trim();
ExternalSOP.finish(process);
if (line.contains(" ")) {
return line.substring(0, line.lastIndexOf(" "));
}
return line;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public String getVersion() {
String[] command = new String[] {binary, "version"};
String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]);
try {
Process process = runtime.exec(command, env);
BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = stdInput.readLine().trim();
ExternalSOP.finish(process);
if (line.contains(" ")) {
return line.substring(line.lastIndexOf(" ") + 1);
}
return line;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public String getBackendVersion() {
String[] command = new String[] {binary, "version", "--backend"};
String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]);
try {
Process process = runtime.exec(command, env);
BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = stdInput.readLine()) != null) {
sb.append(line).append('\n');
}
ExternalSOP.finish(process);
return sb.toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public String getExtendedVersion() {
String[] command = new String[] {binary, "version", "--extended"};
String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]);
try {
Process process = runtime.exec(command, env);
BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = stdInput.readLine()) != null) {
sb.append(line).append('\n');
}
ExternalSOP.finish(process);
return sb.toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public int getSopSpecRevisionNumber() {
String revision = getSopSpecVersion();
String firstLine;
if (revision.contains("\n")) {
firstLine = revision.substring(0, revision.indexOf("\n"));
} else {
firstLine = revision;
}
if (!firstLine.contains("-")) {
return -1;
}
return Integer.parseInt(firstLine.substring(firstLine.lastIndexOf("-") + 1));
}
@Override
public boolean isSopSpecImplementationIncomplete() {
String revision = getSopSpecVersion();
return revision.startsWith("~");
}
@Override
public String getSopSpecImplementationRemarks() {
String revision = getSopSpecVersion();
if (revision.contains("\n")) {
String tail = revision.substring(revision.indexOf("\n") + 1).trim();
if (!tail.isEmpty()) {
return tail;
}
}
return null;
}
@Override
public String getSopSpecVersion() {
String[] command = new String[] {binary, "version", "--sop-spec"};
String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]);
try {
Process process = runtime.exec(command, env);
BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = stdInput.readLine()) != null) {
sb.append(line).append('\n');
}
ExternalSOP.finish(process);
return sb.toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,8 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
/**
* Implementation of sop-java which delegates execution to a binary implementing the SOP command line interface.
*/
package sop.external;

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,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,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.ArmorDearmorTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalArmorDearmorTest extends ArmorDearmorTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.DecryptWithSessionKeyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalDecryptWithSessionKeyTest extends DecryptWithSessionKeyTest {
}

View file

@ -1,12 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.DetachedSignDetachedVerifyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalDetachedSignDetachedVerifyTest extends DetachedSignDetachedVerifyTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.EncryptDecryptTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalEncryptDecryptTest extends EncryptDecryptTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.ExtractCertTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalExtractCertTest extends ExtractCertTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.GenerateKeyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalGenerateKeyTest extends GenerateKeyTest {
}

View file

@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.InlineSignInlineDetachDetachedVerifyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalInlineSignInlineDetachDetachedVerifyTest
extends InlineSignInlineDetachDetachedVerifyTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.InlineSignInlineVerifyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalInlineSignInlineVerifyTest extends InlineSignInlineVerifyTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.ListProfilesTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalListProfilesTest extends ListProfilesTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.RevokeKeyTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalRevokeKeyTest extends RevokeKeyTest {
}

View file

@ -1,13 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.external.operation;
import org.junit.jupiter.api.condition.EnabledIf;
import sop.testsuite.operation.VersionTest;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class ExternalVersionTest extends VersionTest {
}

View file

@ -6,5 +6,7 @@ rootProject.name = 'SOP-Java'
include 'sop-java',
'sop-java-picocli',
'external-sop'
'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

@ -12,19 +12,16 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
// Testing Exit Codes in JUnit
// https://todd.ginsberg.com/post/testing-system-exit/
testImplementation "com.ginsberg:junit5-system-exit:$junitSysExitVersion"
// Mocking Components
testImplementation "org.mockito:mockito-core:$mockitoVersion"
// SOP
implementation(project(":sop-java"))
testImplementation(project(":sop-java-testfixtures"))
// CLI
implementation "info.picocli:picocli:$picocliVersion"
annotationProcessor "info.picocli:picocli-codegen:$picocliVersion"
kapt "info.picocli:picocli-codegen:$picocliVersion"
// @Nonnull, @Nullable...
implementation "com.google.code.findbugs:jsr305:$jsrVersion"
@ -36,8 +33,13 @@ application {
mainClass = mainClassName
}
compileJava {
options.compilerArgs += ["-Aproject=${project.group}/${project.name}"]
}
jar {
dependsOn(":sop-java:jar")
duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
manifest {
attributes 'Main-Class': "$mainClassName"

View file

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

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

View file

@ -1,129 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli;
import picocli.AutoComplete;
import picocli.CommandLine;
import sop.SOP;
import sop.cli.picocli.commands.ArmorCmd;
import sop.cli.picocli.commands.ChangeKeyPasswordCmd;
import sop.cli.picocli.commands.DearmorCmd;
import sop.cli.picocli.commands.DecryptCmd;
import sop.cli.picocli.commands.InlineDetachCmd;
import sop.cli.picocli.commands.EncryptCmd;
import sop.cli.picocli.commands.ExtractCertCmd;
import sop.cli.picocli.commands.GenerateKeyCmd;
import sop.cli.picocli.commands.InlineSignCmd;
import sop.cli.picocli.commands.InlineVerifyCmd;
import sop.cli.picocli.commands.ListProfilesCmd;
import sop.cli.picocli.commands.RevokeKeyCmd;
import sop.cli.picocli.commands.SignCmd;
import sop.cli.picocli.commands.VerifyCmd;
import sop.cli.picocli.commands.VersionCmd;
import sop.exception.SOPGPException;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
@CommandLine.Command(
name = "sop",
resourceBundle = "msg_sop",
exitCodeOnInvalidInput = SOPGPException.UnsupportedSubcommand.EXIT_CODE,
subcommands = {
// Meta Subcommands
VersionCmd.class,
ListProfilesCmd.class,
// Key and Certificate Management Subcommands
GenerateKeyCmd.class,
ChangeKeyPasswordCmd.class,
RevokeKeyCmd.class,
ExtractCertCmd.class,
// Messaging Subcommands
SignCmd.class,
VerifyCmd.class,
EncryptCmd.class,
DecryptCmd.class,
InlineDetachCmd.class,
InlineSignCmd.class,
InlineVerifyCmd.class,
// Transport Subcommands
ArmorCmd.class,
DearmorCmd.class,
// Miscellaneous Subcommands
CommandLine.HelpCommand.class,
AutoComplete.GenerateCompletion.class
}
)
public class SopCLI {
// Singleton
static SOP SOP_INSTANCE;
static ResourceBundle cliMsg = ResourceBundle.getBundle("msg_sop");
public static String EXECUTABLE_NAME = "sop";
@CommandLine.Option(names = {"--stacktrace"},
scope = CommandLine.ScopeType.INHERIT)
static boolean stacktrace;
public static void main(String[] args) {
int exitCode = execute(args);
if (exitCode != 0) {
System.exit(exitCode);
}
}
public static int execute(String[] args) {
// Set locale
new CommandLine(new InitLocale()).parseArgs(args);
// get error message bundle
cliMsg = ResourceBundle.getBundle("msg_sop");
// Prepare CLI
CommandLine cmd = new CommandLine(SopCLI.class);
// explicitly set help command resource bundle
cmd.getSubcommands().get("help").setResourceBundle(ResourceBundle.getBundle("msg_help"));
// Hide generate-completion command
cmd.getSubcommands().get("generate-completion").getCommandSpec().usageMessage().hidden(true);
cmd.setCommandName(EXECUTABLE_NAME)
.setExecutionExceptionHandler(new SOPExecutionExceptionHandler())
.setExitCodeExceptionMapper(new SOPExceptionExitCodeMapper())
.setCaseInsensitiveEnumValuesAllowed(true);
return cmd.execute(args);
}
public static SOP getSop() {
if (SOP_INSTANCE == null) {
String errorMsg = cliMsg.getString("sop.error.runtime.no_backend_set");
throw new IllegalStateException(errorMsg);
}
return SOP_INSTANCE;
}
public static void setSopInstance(SOP instance) {
SOP_INSTANCE = instance;
}
}
/**
* Control the locale.
*
* @see <a href="https://picocli.info/#_controlling_the_locale">Picocli Readme</a>
*/
class InitLocale {
@CommandLine.Option(names = { "-l", "--locale" }, descriptionKey = "sop.locale")
void setLocale(String locale) {
Locale.setDefault(new Locale(locale));
}
@CommandLine.Unmatched
List<String> remainder; // ignore any other parameters and options in the first parsing phase
}

View file

@ -1,282 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import sop.exception.SOPGPException;
import sop.util.UTCUtil;
import sop.util.UTF8Util;
import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.ParseException;
import java.util.Collection;
import java.util.Date;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.regex.Pattern;
/**
* Abstract super class of SOP subcommands.
*/
public abstract class AbstractSopCmd implements Runnable {
/**
* Interface to modularize resolving of environment variables.
*/
public interface EnvironmentVariableResolver {
/**
* Resolve the value of the given environment variable.
* Return null if the variable is not present.
*
* @param name name of the variable
* @return variable value or null
*/
String resolveEnvironmentVariable(String name);
}
public static final String PRFX_ENV = "@ENV:";
public static final String PRFX_FD = "@FD:";
public static final Date BEGINNING_OF_TIME = new Date(0);
public static final Date END_OF_TIME = new Date(8640000000000000L);
public static final Pattern PATTERN_FD = Pattern.compile("^\\d{1,20}$");
protected final ResourceBundle messages;
protected EnvironmentVariableResolver envResolver = System::getenv;
public AbstractSopCmd() {
this(Locale.getDefault());
}
public AbstractSopCmd(@Nonnull Locale locale) {
messages = ResourceBundle.getBundle("msg_sop", locale);
}
void throwIfOutputExists(String output) {
if (output == null) {
return;
}
File outputFile = new File(output);
if (outputFile.exists()) {
String errorMsg = getMsg("sop.error.indirect_data_type.output_file_already_exists", outputFile.getAbsolutePath());
throw new SOPGPException.OutputExists(errorMsg);
}
}
public String getMsg(String key) {
return messages.getString(key);
}
public String getMsg(String key, String arg1) {
return String.format(messages.getString(key), arg1);
}
public String getMsg(String key, String arg1, String arg2) {
return String.format(messages.getString(key), arg1, arg2);
}
void throwIfMissingArg(Object arg, String argName) {
if (arg == null) {
String errorMsg = getMsg("sop.error.usage.argument_required", argName);
throw new SOPGPException.MissingArg(errorMsg);
}
}
void throwIfEmptyParameters(Collection<?> arg, String parmName) {
if (arg.isEmpty()) {
String errorMsg = getMsg("sop.error.usage.parameter_required", parmName);
throw new SOPGPException.MissingArg(errorMsg);
}
}
<T> T throwIfUnsupportedSubcommand(T subcommand, String subcommandName) {
if (subcommand == null) {
String errorMsg = getMsg("sop.error.feature_support.subcommand_not_supported", subcommandName);
throw new SOPGPException.UnsupportedSubcommand(errorMsg);
}
return subcommand;
}
void setEnvironmentVariableResolver(EnvironmentVariableResolver envResolver) {
if (envResolver == null) {
throw new NullPointerException("Variable envResolver cannot be null.");
}
this.envResolver = envResolver;
}
public InputStream getInput(String indirectInput) throws IOException {
if (indirectInput == null) {
throw new IllegalArgumentException("Input cannot not be null.");
}
String trimmed = indirectInput.trim();
if (trimmed.isEmpty()) {
throw new IllegalArgumentException("Input cannot be blank.");
}
if (trimmed.startsWith(PRFX_ENV)) {
if (new File(trimmed).exists()) {
String errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed);
throw new SOPGPException.AmbiguousInput(errorMsg);
}
String envName = trimmed.substring(PRFX_ENV.length());
String envValue = envResolver.resolveEnvironmentVariable(envName);
if (envValue == null) {
String errorMsg = getMsg("sop.error.indirect_data_type.environment_variable_not_set", envName);
throw new IllegalArgumentException(errorMsg);
}
if (envValue.trim().isEmpty()) {
String errorMsg = getMsg("sop.error.indirect_data_type.environment_variable_empty", envName);
throw new IllegalArgumentException(errorMsg);
}
return new ByteArrayInputStream(envValue.getBytes("UTF8"));
} else if (trimmed.startsWith(PRFX_FD)) {
if (new File(trimmed).exists()) {
String errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed);
throw new SOPGPException.AmbiguousInput(errorMsg);
}
File fdFile = fileDescriptorFromString(trimmed);
try {
FileInputStream fileIn = new FileInputStream(fdFile);
return fileIn;
} catch (FileNotFoundException e) {
String errorMsg = getMsg("sop.error.indirect_data_type.file_descriptor_not_found", fdFile.getAbsolutePath());
throw new IOException(errorMsg, e);
}
} else {
File file = new File(trimmed);
if (!file.exists()) {
String errorMsg = getMsg("sop.error.indirect_data_type.input_file_does_not_exist", file.getAbsolutePath());
throw new SOPGPException.MissingInput(errorMsg);
}
if (!file.isFile()) {
String errorMsg = getMsg("sop.error.indirect_data_type.input_not_a_file", file.getAbsolutePath());
throw new SOPGPException.MissingInput(errorMsg);
}
return new FileInputStream(file);
}
}
public OutputStream getOutput(String indirectOutput) throws IOException {
if (indirectOutput == null) {
throw new IllegalArgumentException("Output cannot be null.");
}
String trimmed = indirectOutput.trim();
if (trimmed.isEmpty()) {
throw new IllegalArgumentException("Output cannot be blank.");
}
// @ENV not allowed for output
if (trimmed.startsWith(PRFX_ENV)) {
String errorMsg = getMsg("sop.error.indirect_data_type.illegal_use_of_env_designator");
throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg);
}
// File Descriptor
if (trimmed.startsWith(PRFX_FD)) {
File fdFile = fileDescriptorFromString(trimmed);
try {
FileOutputStream fout = new FileOutputStream(fdFile);
return fout;
} catch (FileNotFoundException e) {
String errorMsg = getMsg("sop.error.indirect_data_type.file_descriptor_not_found", fdFile.getAbsolutePath());
throw new IOException(errorMsg, e);
}
}
File file = new File(trimmed);
if (file.exists()) {
String errorMsg = getMsg("sop.error.indirect_data_type.output_file_already_exists", file.getAbsolutePath());
throw new SOPGPException.OutputExists(errorMsg);
}
if (!file.createNewFile()) {
String errorMsg = getMsg("sop.error.indirect_data_type.output_file_cannot_be_created", file.getAbsolutePath());
throw new IOException(errorMsg);
}
return new FileOutputStream(file);
}
public File fileDescriptorFromString(String fdString) {
File fdDir = new File("/dev/fd/");
if (!fdDir.exists()) {
String errorMsg = getMsg("sop.error.indirect_data_type.designator_fd_not_supported");
throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg);
}
String fdNumber = fdString.substring(PRFX_FD.length());
if (!PATTERN_FD.matcher(fdNumber).matches()) {
throw new IllegalArgumentException("File descriptor must be a positive number.");
}
File descriptor = new File(fdDir, fdNumber);
return descriptor;
}
public static String stringFromInputStream(InputStream inputStream) throws IOException {
try {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
byte[] buf = new byte[4096]; int read;
while ((read = inputStream.read(buf)) != -1) {
byteOut.write(buf, 0, read);
}
// TODO: For decrypt operations we MUST accept non-UTF8 passwords
return UTF8Util.decodeUTF8(byteOut.toByteArray());
} finally {
inputStream.close();
}
}
public Date parseNotAfter(String notAfter) {
if (notAfter.equals("now")) {
return new Date();
}
if (notAfter.equals("-")) {
return END_OF_TIME;
}
try {
return UTCUtil.parseUTCDate(notAfter);
} catch (ParseException e) {
String errorMsg = getMsg("sop.error.input.malformed_not_after");
throw new IllegalArgumentException(errorMsg);
}
}
public Date parseNotBefore(String notBefore) {
if (notBefore.equals("now")) {
return new Date();
}
if (notBefore.equals("-")) {
return BEGINNING_OF_TIME;
}
try {
return UTCUtil.parseUTCDate(notBefore);
} catch (ParseException e) {
String errorMsg = getMsg("sop.error.input.malformed_not_before");
throw new IllegalArgumentException(errorMsg);
}
}
}

View file

@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.Ready;
import sop.cli.picocli.SopCLI;
import sop.enums.ArmorLabel;
import sop.exception.SOPGPException;
import sop.operation.Armor;
import java.io.IOException;
@CommandLine.Command(name = "armor",
resourceBundle = "msg_armor",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class ArmorCmd extends AbstractSopCmd {
@CommandLine.Option(names = {"--label"},
paramLabel = "{auto|sig|key|cert|message}")
ArmorLabel label;
@Override
public void run() {
Armor armor = throwIfUnsupportedSubcommand(
SopCLI.getSop().armor(),
"armor");
if (label != null) {
try {
armor.label(label);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--label");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
}
}
try {
Ready ready = armor.data(System.in);
ready.writeTo(System.out);
} catch (SOPGPException.BadData badData) {
String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data");
throw new SOPGPException.BadData(errorMsg, badData);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,56 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.ChangeKeyPassword;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@CommandLine.Command(name = "change-key-password",
resourceBundle = "msg_change-key-password",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class ChangeKeyPasswordCmd extends AbstractSopCmd {
@CommandLine.Option(names = "--no-armor",
negatable = true)
boolean armor = true;
@CommandLine.Option(names = {"--old-key-password"},
paramLabel = "PASSWORD")
List<String> oldKeyPasswords = new ArrayList<>();
@CommandLine.Option(names = {"--new-key-password"}, arity = "0..1",
paramLabel = "PASSWORD")
String newKeyPassword = null;
@Override
public void run() {
ChangeKeyPassword changeKeyPassword = throwIfUnsupportedSubcommand(
SopCLI.getSop().changeKeyPassword(), "change-key-password");
if (!armor) {
changeKeyPassword.noArmor();
}
for (String oldKeyPassword : oldKeyPasswords) {
changeKeyPassword.oldKeyPassphrase(oldKeyPassword);
}
if (newKeyPassword != null) {
changeKeyPassword.newKeyPassphrase(newKeyPassword);
}
try {
changeKeyPassword.keys(System.in).writeTo(System.out);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,47 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Dearmor;
import java.io.IOException;
@CommandLine.Command(name = "dearmor",
resourceBundle = "msg_dearmor",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class DearmorCmd extends AbstractSopCmd {
@Override
public void run() {
Dearmor dearmor = throwIfUnsupportedSubcommand(
SopCLI.getSop().dearmor(), "dearmor");
try {
dearmor.data(System.in)
.writeTo(System.out);
} catch (SOPGPException.BadData e) {
String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data");
throw new SOPGPException.BadData(errorMsg, e);
} catch (IOException e) {
String msg = e.getMessage();
if (msg == null) {
throw new RuntimeException(e);
}
String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data");
if (msg.equals("invalid armor") ||
msg.equals("invalid armor header") ||
msg.equals("inconsistent line endings in headers") ||
msg.startsWith("unable to decode base64 data")) {
throw new SOPGPException.BadData(errorMsg, e);
}
throw new RuntimeException(e);
}
}
}

View file

@ -1,255 +0,0 @@
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.DecryptionResult;
import sop.ReadyWithResult;
import sop.SessionKey;
import sop.Verification;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Decrypt;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@CommandLine.Command(name = "decrypt",
resourceBundle = "msg_decrypt",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class DecryptCmd extends AbstractSopCmd {
private static final String OPT_SESSION_KEY_OUT = "--session-key-out";
private static final String OPT_WITH_SESSION_KEY = "--with-session-key";
private static final String OPT_WITH_PASSWORD = "--with-password";
private static final String OPT_WITH_KEY_PASSWORD = "--with-key-password";
private static final String OPT_VERIFICATIONS_OUT = "--verifications-out"; // see SOP-05
private static final String OPT_VERIFY_WITH = "--verify-with";
private static final String OPT_NOT_BEFORE = "--verify-not-before";
private static final String OPT_NOT_AFTER = "--verify-not-after";
@CommandLine.Option(
names = {OPT_SESSION_KEY_OUT},
paramLabel = "SESSIONKEY")
String sessionKeyOut;
@CommandLine.Option(
names = {OPT_WITH_SESSION_KEY},
paramLabel = "SESSIONKEY")
List<String> withSessionKey = new ArrayList<>();
@CommandLine.Option(
names = {OPT_WITH_PASSWORD},
paramLabel = "PASSWORD")
List<String> withPassword = new ArrayList<>();
@CommandLine.Option(names = {OPT_VERIFICATIONS_OUT, "--verify-out"}, // TODO: Remove --verify-out in 06
paramLabel = "VERIFICATIONS")
String verifyOut;
@CommandLine.Option(names = {OPT_VERIFY_WITH},
paramLabel = "CERT")
List<String> certs = new ArrayList<>();
@CommandLine.Option(names = {OPT_NOT_BEFORE},
paramLabel = "DATE")
String notBefore = "-";
@CommandLine.Option(names = {OPT_NOT_AFTER},
paramLabel = "DATE")
String notAfter = "now";
@CommandLine.Parameters(index = "0..*",
paramLabel = "KEY")
List<String> keys = new ArrayList<>();
@CommandLine.Option(names = {OPT_WITH_KEY_PASSWORD},
paramLabel = "PASSWORD")
List<String> withKeyPassword = new ArrayList<>();
@Override
public void run() {
Decrypt decrypt = throwIfUnsupportedSubcommand(
SopCLI.getSop().decrypt(), "decrypt");
throwIfOutputExists(verifyOut);
throwIfOutputExists(sessionKeyOut);
setNotAfter(notAfter, decrypt);
setNotBefore(notBefore, decrypt);
setWithPasswords(withPassword, decrypt);
setWithSessionKeys(withSessionKey, decrypt);
setWithKeyPassword(withKeyPassword, decrypt);
setVerifyWith(certs, decrypt);
setDecryptWith(keys, decrypt);
if (verifyOut != null && certs.isEmpty()) {
String errorMsg = getMsg("sop.error.usage.option_requires_other_option", OPT_VERIFICATIONS_OUT, OPT_VERIFY_WITH);
throw new SOPGPException.IncompleteVerification(errorMsg);
}
try {
ReadyWithResult<DecryptionResult> ready = decrypt.ciphertext(System.in);
DecryptionResult result = ready.writeTo(System.out);
writeSessionKeyOut(result);
writeVerifyOut(result);
} catch (SOPGPException.BadData badData) {
String errorMsg = getMsg("sop.error.input.stdin_not_a_message");
throw new SOPGPException.BadData(errorMsg, badData);
} catch (SOPGPException.CannotDecrypt e) {
String errorMsg = getMsg("sop.error.runtime.cannot_decrypt_message");
throw new SOPGPException.CannotDecrypt(errorMsg, e);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
}
}
private void writeVerifyOut(DecryptionResult result) throws IOException {
if (verifyOut != null) {
if (result.getVerifications().isEmpty()) {
String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found");
throw new SOPGPException.NoSignature(errorMsg);
}
try (OutputStream fileOut = getOutput(verifyOut)) {
PrintWriter writer = new PrintWriter(fileOut);
for (Verification verification : result.getVerifications()) {
// CHECKSTYLE:OFF
writer.println(verification.toString());
// CHECKSTYLE:ON
}
writer.flush();
}
}
}
private void writeSessionKeyOut(DecryptionResult result) throws IOException {
if (sessionKeyOut == null) {
return;
}
try (OutputStream outputStream = getOutput(sessionKeyOut)) {
if (!result.getSessionKey().isPresent()) {
String errorMsg = getMsg("sop.error.runtime.no_session_key_extracted");
throw new SOPGPException.UnsupportedOption(String.format(errorMsg, OPT_SESSION_KEY_OUT));
}
SessionKey sessionKey = result.getSessionKey().get();
PrintWriter writer = new PrintWriter(outputStream);
// CHECKSTYLE:OFF
writer.println(sessionKey.toString());
// CHECKSTYLE:ON
writer.flush();
}
}
private void setDecryptWith(List<String> keys, Decrypt decrypt) {
for (String key : keys) {
try (InputStream keyIn = getInput(key)) {
decrypt.withKey(keyIn);
} catch (SOPGPException.KeyIsProtected keyIsProtected) {
String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", key);
throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected);
} catch (SOPGPException.BadData badData) {
String errorMsg = getMsg("sop.error.input.not_a_private_key", key);
throw new SOPGPException.BadData(errorMsg, badData);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private void setVerifyWith(List<String> certs, Decrypt decrypt) {
for (String cert : certs) {
try (InputStream certIn = getInput(cert)) {
decrypt.verifyWithCert(certIn);
} catch (SOPGPException.BadData badData) {
String errorMsg = getMsg("sop.error.input.not_a_certificate", cert);
throw new SOPGPException.BadData(errorMsg, badData);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
}
}
}
private void setWithSessionKeys(List<String> withSessionKey, Decrypt decrypt) {
for (String sessionKeyFile : withSessionKey) {
String sessionKeyString;
try {
sessionKeyString = stringFromInputStream(getInput(sessionKeyFile));
} catch (IOException e) {
throw new RuntimeException(e);
}
SessionKey sessionKey;
try {
sessionKey = SessionKey.fromString(sessionKeyString);
} catch (IllegalArgumentException e) {
String errorMsg = getMsg("sop.error.input.malformed_session_key");
throw new IllegalArgumentException(errorMsg, e);
}
try {
decrypt.withSessionKey(sessionKey);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_SESSION_KEY);
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
}
}
}
private void setWithPasswords(List<String> withPassword, Decrypt decrypt) {
for (String passwordFile : withPassword) {
try {
String password = stringFromInputStream(getInput(passwordFile));
decrypt.withPassword(password);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_PASSWORD);
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private void setWithKeyPassword(List<String> withKeyPassword, Decrypt decrypt) {
for (String passwordFile : withKeyPassword) {
try {
String password = stringFromInputStream(getInput(passwordFile));
decrypt.withKeyPassword(password);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_KEY_PASSWORD);
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private void setNotAfter(String notAfter, Decrypt decrypt) {
Date notAfterDate = parseNotAfter(notAfter);
try {
decrypt.verifyNotAfter(notAfterDate);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_AFTER);
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
}
}
private void setNotBefore(String notBefore, Decrypt decrypt) {
Date notBeforeDate = parseNotBefore(notBefore);
try {
decrypt.verifyNotBefore(notBeforeDate);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_BEFORE);
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
}
}
}

View file

@ -1,154 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.Ready;
import sop.cli.picocli.SopCLI;
import sop.enums.EncryptAs;
import sop.exception.SOPGPException;
import sop.operation.Encrypt;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@CommandLine.Command(name = "encrypt",
resourceBundle = "msg_encrypt",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class EncryptCmd extends AbstractSopCmd {
@CommandLine.Option(names = "--no-armor",
negatable = true)
boolean armor = true;
@CommandLine.Option(names = {"--as"},
paramLabel = "{binary|text}")
EncryptAs type;
@CommandLine.Option(names = "--with-password",
paramLabel = "PASSWORD")
List<String> withPassword = new ArrayList<>();
@CommandLine.Option(names = "--sign-with",
paramLabel = "KEY")
List<String> signWith = new ArrayList<>();
@CommandLine.Option(names = "--with-key-password",
paramLabel = "PASSWORD")
List<String> withKeyPassword = new ArrayList<>();
@CommandLine.Option(names = "--profile",
paramLabel = "PROFILE")
String profile;
@CommandLine.Parameters(index = "0..*",
paramLabel = "CERTS")
List<String> certs = new ArrayList<>();
@Override
public void run() {
Encrypt encrypt = throwIfUnsupportedSubcommand(
SopCLI.getSop().encrypt(), "encrypt");
if (profile != null) {
try {
encrypt.profile(profile);
} catch (SOPGPException.UnsupportedProfile e) {
String errorMsg = getMsg("sop.error.usage.profile_not_supported", "encrypt", profile);
throw new SOPGPException.UnsupportedProfile(errorMsg, e);
}
}
if (type != null) {
try {
encrypt.mode(type);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
}
}
if (withPassword.isEmpty() && certs.isEmpty()) {
String errorMsg = getMsg("sop.error.usage.password_or_cert_required");
throw new SOPGPException.MissingArg(errorMsg);
}
for (String passwordFileName : withPassword) {
try {
String password = stringFromInputStream(getInput(passwordFileName));
encrypt.withPassword(password);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-password");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
for (String passwordFileName : withKeyPassword) {
try {
String password = stringFromInputStream(getInput(passwordFileName));
encrypt.withKeyPassword(password);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
for (String keyInput : signWith) {
try (InputStream keyIn = getInput(keyInput)) {
encrypt.signWith(keyIn);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (SOPGPException.KeyIsProtected keyIsProtected) {
String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput);
throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected);
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
String errorMsg = getMsg("sop.error.runtime.key_uses_unsupported_asymmetric_algorithm", keyInput);
throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo);
} catch (SOPGPException.KeyCannotSign keyCannotSign) {
String errorMsg = getMsg("sop.error.runtime.key_cannot_sign", keyInput);
throw new SOPGPException.KeyCannotSign(errorMsg, keyCannotSign);
} catch (SOPGPException.BadData badData) {
String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput);
throw new SOPGPException.BadData(errorMsg, badData);
}
}
for (String certInput : certs) {
try (InputStream certIn = getInput(certInput)) {
encrypt.withCert(certIn);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
String errorMsg = getMsg("sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput);
throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo);
} catch (SOPGPException.CertCannotEncrypt certCannotEncrypt) {
String errorMsg = getMsg("sop.error.runtime.cert_cannot_encrypt", certInput);
throw new SOPGPException.CertCannotEncrypt(errorMsg, certCannotEncrypt);
} catch (SOPGPException.BadData badData) {
String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput);
throw new SOPGPException.BadData(errorMsg, badData);
}
}
if (!armor) {
encrypt.noArmor();
}
try {
Ready ready = encrypt.plaintext(System.in);
ready.writeTo(System.out);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,43 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import java.io.IOException;
import picocli.CommandLine;
import sop.Ready;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.ExtractCert;
@CommandLine.Command(name = "extract-cert",
resourceBundle = "msg_extract-cert",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class ExtractCertCmd extends AbstractSopCmd {
@CommandLine.Option(names = "--no-armor",
negatable = true)
boolean armor = true;
@Override
public void run() {
ExtractCert extractCert = throwIfUnsupportedSubcommand(
SopCLI.getSop().extractCert(), "extract-cert");
if (!armor) {
extractCert.noArmor();
}
try {
Ready ready = extractCert.key(System.in);
ready.writeTo(System.out);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (SOPGPException.BadData badData) {
String errorMsg = getMsg("sop.error.input.stdin_not_a_private_key");
throw new SOPGPException.BadData(errorMsg, badData);
}
}
}

View file

@ -1,85 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.Ready;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.GenerateKey;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@CommandLine.Command(name = "generate-key",
resourceBundle = "msg_generate-key",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class GenerateKeyCmd extends AbstractSopCmd {
@CommandLine.Option(names = "--no-armor",
negatable = true)
boolean armor = true;
@CommandLine.Parameters(paramLabel = "USERID")
List<String> userId = new ArrayList<>();
@CommandLine.Option(names = "--with-key-password",
paramLabel = "PASSWORD")
String withKeyPassword;
@CommandLine.Option(names = "--profile",
paramLabel = "PROFILE")
String profile;
@CommandLine.Option(names = "--signing-only")
boolean signingOnly = false;
@Override
public void run() {
GenerateKey generateKey = throwIfUnsupportedSubcommand(
SopCLI.getSop().generateKey(), "generate-key");
if (profile != null) {
try {
generateKey.profile(profile);
} catch (SOPGPException.UnsupportedProfile e) {
String errorMsg = getMsg("sop.error.usage.profile_not_supported", "generate-key", profile);
throw new SOPGPException.UnsupportedProfile(errorMsg, e);
}
}
if (signingOnly) {
generateKey.signingOnly();
}
for (String userId : userId) {
generateKey.userId(userId);
}
if (!armor) {
generateKey.noArmor();
}
if (withKeyPassword != null) {
try {
String password = stringFromInputStream(getInput(withKeyPassword));
generateKey.withKeyPassword(password);
} catch (SOPGPException.UnsupportedOption e) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password");
throw new SOPGPException.UnsupportedOption(errorMsg, e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
try {
Ready ready = generateKey.generate();
ready.writeTo(System.out);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.Signatures;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.InlineDetach;
import java.io.IOException;
import java.io.OutputStream;
@CommandLine.Command(name = "inline-detach",
resourceBundle = "msg_inline-detach",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class InlineDetachCmd extends AbstractSopCmd {
@CommandLine.Option(
names = {"--signatures-out"},
paramLabel = "SIGNATURES")
String signaturesOut;
@CommandLine.Option(names = "--no-armor",
negatable = true)
boolean armor = true;
@Override
public void run() {
InlineDetach inlineDetach = throwIfUnsupportedSubcommand(
SopCLI.getSop().inlineDetach(), "inline-detach");
throwIfOutputExists(signaturesOut);
throwIfMissingArg(signaturesOut, "--signatures-out");
if (!armor) {
inlineDetach.noArmor();
}
try (OutputStream outputStream = getOutput(signaturesOut)) {
Signatures signatures = inlineDetach
.message(System.in).writeTo(System.out);
signatures.writeTo(outputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,101 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.Ready;
import sop.cli.picocli.SopCLI;
import sop.enums.InlineSignAs;
import sop.exception.SOPGPException;
import sop.operation.InlineSign;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@CommandLine.Command(name = "inline-sign",
resourceBundle = "msg_inline-sign",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class InlineSignCmd extends AbstractSopCmd {
@CommandLine.Option(names = "--no-armor",
negatable = true)
boolean armor = true;
@CommandLine.Option(names = "--as",
paramLabel = "{binary|text|clearsigned}")
InlineSignAs type;
@CommandLine.Parameters(paramLabel = "KEYS")
List<String> secretKeyFile = new ArrayList<>();
@CommandLine.Option(names = "--with-key-password",
paramLabel = "PASSWORD")
List<String> withKeyPassword = new ArrayList<>();
@Override
public void run() {
InlineSign inlineSign = throwIfUnsupportedSubcommand(
SopCLI.getSop().inlineSign(), "inline-sign");
// Clearsigned messages are inherently armored, so --no-armor makes no sense.
if (!armor && type == InlineSignAs.clearsigned) {
String errorMsg = getMsg("sop.error.usage.incompatible_options.clearsigned_no_armor");
throw new SOPGPException.IncompatibleOptions(errorMsg);
}
if (type != null) {
try {
inlineSign.mode(type);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
}
}
if (secretKeyFile.isEmpty()) {
String errorMsg = getMsg("sop.error.usage.parameter_required", "KEYS");
throw new SOPGPException.MissingArg(errorMsg);
}
for (String passwordFile : withKeyPassword) {
try {
String password = stringFromInputStream(getInput(passwordFile));
inlineSign.withKeyPassword(password);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
for (String keyInput : secretKeyFile) {
try (InputStream keyIn = getInput(keyInput)) {
inlineSign.key(keyIn);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (SOPGPException.KeyIsProtected e) {
String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput);
throw new SOPGPException.KeyIsProtected(errorMsg, e);
} catch (SOPGPException.BadData badData) {
String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput);
throw new SOPGPException.BadData(errorMsg, badData);
}
}
if (!armor) {
inlineSign.noArmor();
}
try {
Ready ready = inlineSign.data(System.in);
ready.writeTo(System.out);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,108 +0,0 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.ReadyWithResult;
import sop.Verification;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.InlineVerify;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
@CommandLine.Command(name = "inline-verify",
resourceBundle = "msg_inline-verify",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class InlineVerifyCmd extends AbstractSopCmd {
@CommandLine.Parameters(arity = "0..*",
paramLabel = "CERT")
List<String> certificates = new ArrayList<>();
@CommandLine.Option(names = {"--not-before"},
paramLabel = "DATE")
String notBefore = "-";
@CommandLine.Option(names = {"--not-after"},
paramLabel = "DATE")
String notAfter = "now";
@CommandLine.Option(names = "--verifications-out", paramLabel = "VERIFICATIONS")
String verificationsOut;
@Override
public void run() {
InlineVerify inlineVerify = throwIfUnsupportedSubcommand(
SopCLI.getSop().inlineVerify(), "inline-verify");
throwIfOutputExists(verificationsOut);
if (notAfter != null) {
try {
inlineVerify.notAfter(parseNotAfter(notAfter));
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
}
}
if (notBefore != null) {
try {
inlineVerify.notBefore(parseNotBefore(notBefore));
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
}
}
for (String certInput : certificates) {
try (InputStream certIn = getInput(certInput)) {
inlineVerify.cert(certIn);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
} catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) {
String errorMsg = getMsg("sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput);
throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo);
} catch (SOPGPException.BadData badData) {
String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput);
throw new SOPGPException.BadData(errorMsg, badData);
}
}
List<Verification> verifications = null;
try {
ReadyWithResult<List<Verification>> ready = inlineVerify.data(System.in);
verifications = ready.writeTo(System.out);
} catch (SOPGPException.NoSignature e) {
String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found");
throw new SOPGPException.NoSignature(errorMsg, e);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
} catch (SOPGPException.BadData badData) {
String errorMsg = getMsg("sop.error.input.stdin_not_a_message");
throw new SOPGPException.BadData(errorMsg, badData);
}
if (verificationsOut != null) {
try (OutputStream outputStream = getOutput(verificationsOut)) {
PrintWriter pw = new PrintWriter(outputStream);
for (Verification verification : verifications) {
// CHECKSTYLE:OFF
pw.println(verification);
// CHECKSTYLE:ON
}
pw.flush();
pw.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

View file

@ -1,36 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.Profile;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.ListProfiles;
@CommandLine.Command(name = "list-profiles",
resourceBundle = "msg_list-profiles",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class ListProfilesCmd extends AbstractSopCmd {
@CommandLine.Parameters(paramLabel = "COMMAND", arity = "1", descriptionKey = "subcommand")
String subcommand;
@Override
public void run() {
ListProfiles listProfiles = throwIfUnsupportedSubcommand(
SopCLI.getSop().listProfiles(), "list-profiles");
try {
for (Profile profile : listProfiles.subcommand(subcommand)) {
Print.outln(profile.toString());
}
} catch (SOPGPException.UnsupportedProfile e) {
String errorMsg = getMsg("sop.error.feature_support.subcommand_does_not_support_profiles", subcommand);
throw new SOPGPException.UnsupportedProfile(errorMsg, e);
}
}
}

View file

@ -1,62 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.Ready;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.RevokeKey;
import java.io.IOException;
@CommandLine.Command(name = "revoke-key",
resourceBundle = "msg_revoke-key",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class RevokeKeyCmd extends AbstractSopCmd {
@CommandLine.Option(names = "--no-armor",
negatable = true)
boolean armor = true;
@CommandLine.Option(names = "--with-key-password",
paramLabel = "PASSWORD")
String withKeyPassword;
@Override
public void run() {
RevokeKey revokeKey = throwIfUnsupportedSubcommand(
SopCLI.getSop().revokeKey(), "revoke-key");
if (!armor) {
revokeKey.noArmor();
}
if (withKeyPassword != null) {
try {
String password = stringFromInputStream(getInput(withKeyPassword));
revokeKey.withKeyPassword(password);
} catch (SOPGPException.UnsupportedOption e) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password");
throw new SOPGPException.UnsupportedOption(errorMsg, e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Ready ready;
try {
ready = revokeKey.keys(System.in);
} catch (SOPGPException.KeyIsProtected e) {
String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", "STANDARD_IN");
throw new SOPGPException.KeyIsProtected(errorMsg, e);
}
try {
ready.writeTo(System.out);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,108 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.MicAlg;
import sop.ReadyWithResult;
import sop.SigningResult;
import sop.cli.picocli.SopCLI;
import sop.enums.SignAs;
import sop.exception.SOPGPException;
import sop.operation.DetachedSign;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
@CommandLine.Command(name = "sign",
resourceBundle = "msg_detached-sign",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class SignCmd extends AbstractSopCmd {
@CommandLine.Option(names = "--no-armor",
negatable = true)
boolean armor = true;
@CommandLine.Option(names = "--as",
paramLabel = "{binary|text}")
SignAs type;
@CommandLine.Parameters(paramLabel = "KEYS")
List<String> secretKeyFile = new ArrayList<>();
@CommandLine.Option(names = "--with-key-password",
paramLabel = "PASSWORD")
List<String> withKeyPassword = new ArrayList<>();
@CommandLine.Option(names = "--micalg-out",
paramLabel = "MICALG")
String micAlgOut;
@Override
public void run() {
DetachedSign detachedSign = throwIfUnsupportedSubcommand(
SopCLI.getSop().detachedSign(), "sign");
throwIfOutputExists(micAlgOut);
throwIfEmptyParameters(secretKeyFile, "KEYS");
if (type != null) {
try {
detachedSign.mode(type);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
}
}
for (String passwordFile : withKeyPassword) {
try {
String password = stringFromInputStream(getInput(passwordFile));
detachedSign.withKeyPassword(password);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
for (String keyInput : secretKeyFile) {
try (InputStream keyIn = getInput(keyInput)) {
detachedSign.key(keyIn);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (SOPGPException.KeyIsProtected keyIsProtected) {
String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput);
throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected);
} catch (SOPGPException.BadData badData) {
String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput);
throw new SOPGPException.BadData(errorMsg, badData);
}
}
if (!armor) {
detachedSign.noArmor();
}
try {
ReadyWithResult<SigningResult> ready = detachedSign.data(System.in);
SigningResult result = ready.writeTo(System.out);
MicAlg micAlg = result.getMicAlg();
if (micAlgOut != null) {
// Write micalg out
OutputStream outputStream = getOutput(micAlgOut);
micAlg.writeTo(outputStream);
outputStream.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -1,102 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.Verification;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.DetachedVerify;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@CommandLine.Command(name = "verify",
resourceBundle = "msg_detached-verify",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class VerifyCmd extends AbstractSopCmd {
@CommandLine.Parameters(index = "0",
paramLabel = "SIGNATURE")
String signature;
@CommandLine.Parameters(index = "1..*",
arity = "1..*",
paramLabel = "CERT")
List<String> certificates = new ArrayList<>();
@CommandLine.Option(names = {"--not-before"},
paramLabel = "DATE")
String notBefore = "-";
@CommandLine.Option(names = {"--not-after"},
paramLabel = "DATE")
String notAfter = "now";
@Override
public void run() {
DetachedVerify detachedVerify = throwIfUnsupportedSubcommand(
SopCLI.getSop().detachedVerify(), "verify");
if (notAfter != null) {
try {
detachedVerify.notAfter(parseNotAfter(notAfter));
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
}
}
if (notBefore != null) {
try {
detachedVerify.notBefore(parseNotBefore(notBefore));
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before");
throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption);
}
}
for (String certInput : certificates) {
try (InputStream certIn = getInput(certInput)) {
detachedVerify.cert(certIn);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
} catch (SOPGPException.BadData badData) {
String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput);
throw new SOPGPException.BadData(errorMsg, badData);
}
}
if (signature != null) {
try (InputStream sigIn = getInput(signature)) {
detachedVerify.signatures(sigIn);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (SOPGPException.BadData badData) {
String errorMsg = getMsg("sop.error.input.not_a_signature", signature);
throw new SOPGPException.BadData(errorMsg, badData);
}
}
List<Verification> verifications;
try {
verifications = detachedVerify.data(System.in);
} catch (SOPGPException.NoSignature e) {
String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found");
throw new SOPGPException.NoSignature(errorMsg, e);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
} catch (SOPGPException.BadData badData) {
String errorMsg = getMsg("sop.error.input.stdin_not_a_message");
throw new SOPGPException.BadData(errorMsg, badData);
}
for (Verification verification : verifications) {
Print.outln(verification.toString());
}
}
}

View file

@ -1,58 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli.commands;
import picocli.CommandLine;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Version;
@CommandLine.Command(name = "version", resourceBundle = "msg_version",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class VersionCmd extends AbstractSopCmd {
@CommandLine.ArgGroup()
Exclusive exclusive;
static class Exclusive {
@CommandLine.Option(names = "--extended")
boolean extended;
@CommandLine.Option(names = "--backend")
boolean backend;
@CommandLine.Option(names = "--sop-spec")
boolean sopSpec;
}
@Override
public void run() {
Version version = throwIfUnsupportedSubcommand(
SopCLI.getSop().version(), "version");
if (exclusive == null) {
Print.outln(version.getName() + " " + version.getVersion());
return;
}
if (exclusive.extended) {
Print.outln(version.getExtendedVersion());
return;
}
if (exclusive.backend) {
Print.outln(version.getBackendVersion());
return;
}
if (exclusive.sopSpec) {
Print.outln(version.getSopSpecVersion());
return;
}
}
}

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

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