├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── .woodpecker ├── build.yml └── reuse.yml ├── CHANGELOG.md ├── LICENSE ├── LICENSES ├── Apache-2.0.txt └── CC0-1.0.txt ├── README.md ├── REUSE.toml ├── build.gradle ├── config └── checkstyle │ ├── checkstyle.xml │ └── suppressions.xml ├── external-sop ├── README.md ├── build.gradle └── src │ ├── main │ ├── kotlin │ │ └── sop │ │ │ └── external │ │ │ ├── ExternalSOP.kt │ │ │ ├── ExternalSOPV.kt │ │ │ └── operation │ │ │ ├── ArmorExternal.kt │ │ │ ├── ChangeKeyPasswordExternal.kt │ │ │ ├── DearmorExternal.kt │ │ │ ├── DecryptExternal.kt │ │ │ ├── DetachedSignExternal.kt │ │ │ ├── DetachedVerifyExternal.kt │ │ │ ├── EncryptExternal.kt │ │ │ ├── ExtractCertExternal.kt │ │ │ ├── GenerateKeyExternal.kt │ │ │ ├── InlineDetachExternal.kt │ │ │ ├── InlineSignExternal.kt │ │ │ ├── InlineVerifyExternal.kt │ │ │ ├── ListProfilesExternal.kt │ │ │ ├── RevokeKeyExternal.kt │ │ │ └── VersionExternal.kt │ └── resources │ │ └── sop │ │ └── testsuite │ │ └── external │ │ ├── .gitignore │ │ ├── config.json.ci │ │ └── config.json.example │ └── test │ └── java │ └── sop │ └── testsuite │ └── external │ ├── ExternalSOPInstanceFactory.java │ └── operation │ ├── ExternalArmorDearmorTest.java │ ├── ExternalDecryptWithSessionKeyTest.java │ ├── ExternalDetachedSignDetachedVerifyTest.java │ ├── ExternalEncryptDecryptTest.java │ ├── ExternalExtractCertTest.java │ ├── ExternalGenerateKeyTest.java │ ├── ExternalInlineSignInlineDetachDetachedVerifyTest.java │ ├── ExternalInlineSignInlineVerifyTest.java │ ├── ExternalListProfilesTest.java │ ├── ExternalRevokeKeyTest.java │ └── ExternalVersionTest.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── sop-java-picocli ├── README.md ├── build.gradle └── src │ ├── main │ ├── kotlin │ │ └── sop │ │ │ └── cli │ │ │ └── picocli │ │ │ ├── SOPExceptionExitCodeMapper.kt │ │ │ ├── SOPExecutionExceptionHandler.kt │ │ │ ├── SopCLI.kt │ │ │ ├── SopVCLI.kt │ │ │ └── commands │ │ │ ├── AbstractSopCmd.kt │ │ │ ├── ArmorCmd.kt │ │ │ ├── ChangeKeyPasswordCmd.kt │ │ │ ├── DearmorCmd.kt │ │ │ ├── DecryptCmd.kt │ │ │ ├── EncryptCmd.kt │ │ │ ├── ExtractCertCmd.kt │ │ │ ├── GenerateKeyCmd.kt │ │ │ ├── InlineDetachCmd.kt │ │ │ ├── InlineSignCmd.kt │ │ │ ├── InlineVerifyCmd.kt │ │ │ ├── ListProfilesCmd.kt │ │ │ ├── RevokeKeyCmd.kt │ │ │ ├── SignCmd.kt │ │ │ ├── VerifyCmd.kt │ │ │ └── VersionCmd.kt │ └── resources │ │ ├── msg_armor.properties │ │ ├── msg_armor_de.properties │ │ ├── msg_change-key-password.properties │ │ ├── msg_change-key-password_de.properties │ │ ├── msg_dearmor.properties │ │ ├── msg_dearmor_de.properties │ │ ├── msg_decrypt.properties │ │ ├── msg_decrypt_de.properties │ │ ├── msg_detached-sign.properties │ │ ├── msg_detached-sign_de.properties │ │ ├── msg_detached-verify.properties │ │ ├── msg_detached-verify_de.properties │ │ ├── msg_encrypt.properties │ │ ├── msg_encrypt_de.properties │ │ ├── msg_extract-cert.properties │ │ ├── msg_extract-cert_de.properties │ │ ├── msg_generate-key.properties │ │ ├── msg_generate-key_de.properties │ │ ├── msg_help.properties │ │ ├── msg_help_de.properties │ │ ├── msg_inline-detach.properties │ │ ├── msg_inline-detach_de.properties │ │ ├── msg_inline-sign.properties │ │ ├── msg_inline-sign_de.properties │ │ ├── msg_inline-verify.properties │ │ ├── msg_inline-verify_de.properties │ │ ├── msg_list-profiles.properties │ │ ├── msg_list-profiles_de.properties │ │ ├── msg_revoke-key.properties │ │ ├── msg_revoke-key_de.properties │ │ ├── msg_sop.properties │ │ ├── msg_sop_de.properties │ │ ├── msg_version.properties │ │ └── msg_version_de.properties │ └── test │ └── java │ └── sop │ └── cli │ └── picocli │ ├── DateParsingTest.java │ ├── SOPTest.java │ ├── TestFileUtil.java │ └── commands │ ├── AbstractSopCmdTest.java │ ├── ArmorCmdTest.java │ ├── DearmorCmdTest.java │ ├── DecryptCmdTest.java │ ├── EncryptCmdTest.java │ ├── ExtractCertCmdTest.java │ ├── GenerateKeyCmdTest.java │ ├── InlineDetachCmdTest.java │ ├── SignCmdTest.java │ ├── TestEnvironmentVariableResolver.java │ ├── VerifyCmdTest.java │ └── VersionCmdTest.java ├── sop-java-testfixtures ├── build.gradle └── src │ └── main │ └── java │ └── sop │ └── testsuite │ ├── AbortOnUnsupportedOption.java │ ├── AbortOnUnsupportedOptionExtension.java │ ├── JUtils.java │ ├── SOPInstanceFactory.java │ ├── TestData.java │ ├── assertions │ ├── SopExecutionAssertions.java │ ├── VerificationAssert.java │ ├── VerificationListAssert.java │ └── package-info.java │ ├── operation │ ├── AbstractSOPTest.java │ ├── ArmorDearmorTest.java │ ├── ChangeKeyPasswordTest.java │ ├── DecryptWithSessionKeyTest.java │ ├── DetachedSignDetachedVerifyTest.java │ ├── EncryptDecryptTest.java │ ├── ExtractCertTest.java │ ├── GenerateKeyTest.java │ ├── InlineSignInlineDetachDetachedVerifyTest.java │ ├── InlineSignInlineVerifyTest.java │ ├── ListProfilesTest.java │ ├── RevokeKeyTest.java │ ├── VersionTest.java │ └── package-info.java │ └── package-info.java ├── sop-java ├── README.md ├── build.gradle └── src │ ├── main │ ├── kotlin │ │ └── sop │ │ │ ├── ByteArrayAndResult.kt │ │ │ ├── DecryptionResult.kt │ │ │ ├── EncryptionResult.kt │ │ │ ├── MicAlg.kt │ │ │ ├── Profile.kt │ │ │ ├── Ready.kt │ │ │ ├── ReadyWithResult.kt │ │ │ ├── SOP.kt │ │ │ ├── SOPV.kt │ │ │ ├── SessionKey.kt │ │ │ ├── Signatures.kt │ │ │ ├── SigningResult.kt │ │ │ ├── Verification.kt │ │ │ ├── enums │ │ │ ├── EncryptAs.kt │ │ │ ├── InlineSignAs.kt │ │ │ ├── SignAs.kt │ │ │ └── SignatureMode.kt │ │ │ ├── exception │ │ │ └── SOPGPException.kt │ │ │ ├── operation │ │ │ ├── AbstractSign.kt │ │ │ ├── AbstractVerify.kt │ │ │ ├── Armor.kt │ │ │ ├── ChangeKeyPassword.kt │ │ │ ├── Dearmor.kt │ │ │ ├── Decrypt.kt │ │ │ ├── DetachedSign.kt │ │ │ ├── DetachedVerify.kt │ │ │ ├── Encrypt.kt │ │ │ ├── ExtractCert.kt │ │ │ ├── GenerateKey.kt │ │ │ ├── InlineDetach.kt │ │ │ ├── InlineSign.kt │ │ │ ├── InlineVerify.kt │ │ │ ├── ListProfiles.kt │ │ │ ├── RevokeKey.kt │ │ │ ├── VerifySignatures.kt │ │ │ └── Version.kt │ │ │ └── util │ │ │ ├── HexUtil.kt │ │ │ ├── Optional.kt │ │ │ ├── ProxyOutputStream.kt │ │ │ ├── UTCUtil.kt │ │ │ └── UTF8Util.kt │ └── resources │ │ └── sop-java-version.properties │ └── test │ └── java │ └── sop │ ├── ByteArrayAndResultTest.java │ ├── MicAlgTest.java │ ├── ProfileTest.java │ ├── ReadyTest.java │ ├── ReadyWithResultTest.java │ ├── SessionKeyTest.java │ ├── SigningResultTest.java │ ├── VerificationTest.java │ └── util │ ├── HexUtilTest.java │ ├── OptionalTest.java │ ├── ProxyOutputStreamTest.java │ ├── UTCUtilTest.java │ └── UTF8UtilTest.java └── version.gradle /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **Version** 14 | 15 | - `sop-java`: 16 | - `pgpainless-core`: 17 | - `bouncycastle`: 18 | 19 | **To Reproduce** 20 | 21 | ``` 22 | Example Code Block 23 | ``` 24 | 25 | **Expected behavior** 26 | 27 | 28 | **Additional context** 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | .idea 6 | .gradle 7 | 8 | out/ 9 | build/ 10 | bin/ 11 | libs/ 12 | 13 | */build 14 | 15 | *.iws 16 | *.iml 17 | *.ipr 18 | *.class 19 | *.log 20 | *.jar 21 | 22 | gradle.properties 23 | !gradle-wrapper.jar 24 | 25 | .classpath 26 | .project 27 | .settings/ 28 | 29 | pgpainless-core/.classpath 30 | pgpainless-core/.project 31 | pgpainless-core/.settings/ 32 | 33 | push_html.sh 34 | -------------------------------------------------------------------------------- /.woodpecker/build.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | run: 3 | when: 4 | event: push 5 | image: gradle:7.6-jdk11-jammy 6 | commands: 7 | # Install Sequoia-SOP 8 | - apt update && apt install --yes sqop 9 | # Checkout code 10 | - git checkout $CI_COMMIT_BRANCH 11 | # Prepare CI 12 | - cp external-sop/src/main/resources/sop/testsuite/external/config.json.ci external-sop/src/main/resources/sop/testsuite/external/config.json 13 | # Code works 14 | - gradle test 15 | # Code is clean 16 | - gradle check javadocAll 17 | # Code has coverage 18 | - gradle jacocoRootReport coveralls 19 | environment: 20 | COVERALLS_REPO_TOKEN: 21 | from_secret: coveralls_repo_token 22 | -------------------------------------------------------------------------------- /.woodpecker/reuse.yml: -------------------------------------------------------------------------------- 1 | # Code is licensed properly 2 | # See https://reuse.software/ 3 | steps: 4 | reuse: 5 | when: 6 | event: push 7 | image: fsfe/reuse:latest 8 | commands: 9 | - reuse lint 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # SOP for Java 8 | 9 | [![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java) 10 | [![Spec Revision: 10](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/10/) 11 | [![Coverage Status](https://coveralls.io/repos/github/pgpainless/sop-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/sop-java?branch=main) 12 | [![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/sop-java)](https://api.reuse.software/info/github.com/pgpainless/sop-java) 13 | 14 | The [Stateless OpenPGP Protocol](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) specification 15 | defines a generic stateless CLI for dealing with OpenPGP messages. 16 | Its goal is to provide a minimal, yet powerful API for the most common OpenPGP related operations. 17 | 18 | [![Packaging status](https://repology.org/badge/vertical-allrepos/sop-java.svg)](https://repology.org/project/pgpainless/versions) 19 | [![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/sop-java)](https://search.maven.org/artifact/org.pgpainless/sop-java) 20 | 21 | ## Modules 22 | 23 | The repository contains the following modules: 24 | 25 | * [sop-java](/sop-java) defines a set of Java interfaces describing the Stateless OpenPGP Protocol. 26 | * [sop-java-picocli](/sop-java-picocli) contains a wrapper application that transforms the `sop-java` API into a command line application 27 | compatible with the SOP-CLI specification. 28 | * [external-sop](/external-sop) contains an API implementation that can be used to forward API calls to a SOP executable, 29 | allowing to delegate the implementation logic to an arbitrary SOP CLI implementation. 30 | * [sop-java-testfixtures](/sop-java-testfixtures) contains a test suite that can be shared by downstream implementations 31 | of `sop-java`. 32 | 33 | ## Known Implementations 34 | (Please expand!) 35 | 36 | | Project | Description | 37 | |-------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------| 38 | | [pgpainless-sop](https://github.com/pgpainless/pgpainless/tree/main/pgpainless-sop) | Implementation of `sop-java` using PGPainless | 39 | | [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` | 40 | | [bcsop](https://codeberg.org/PGPainless/bc-sop) | Implementation of `sop-java` using vanilla Bouncy Castle | 41 | 42 | ### Implementations in other languages 43 | | Project | Language | 44 | |---------------------------------------------------|----------| 45 | | [sop-rs](https://sequoia-pgp.gitlab.io/sop-rs/) | Rust | 46 | | [SOP for python](https://pypi.org/project/sop/) | Python | 47 | | [rpgpie-sop](https://crates.io/crates/rpgpie-sop) | Rust | 48 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | version = 1 6 | SPDX-PackageName = "SOP-Java" 7 | SPDX-PackageSupplier = "Paul Schaub " 8 | SPDX-PackageDownloadLocation = "https://pgpainless.org" 9 | 10 | [[annotations]] 11 | path = "gradle**" 12 | precedence = "aggregate" 13 | SPDX-FileCopyrightText = "2015 the original author or authors." 14 | SPDX-License-Identifier = "Apache-2.0" 15 | 16 | [[annotations]] 17 | path = ".woodpecker/**" 18 | precedence = "aggregate" 19 | SPDX-FileCopyrightText = "2022 the original author or authors." 20 | SPDX-License-Identifier = "Apache-2.0" 21 | 22 | [[annotations]] 23 | path = "external-sop/src/main/resources/sop/testsuite/external/**" 24 | precedence = "aggregate" 25 | SPDX-FileCopyrightText = "2023 the original author or authors" 26 | SPDX-License-Identifier = "Apache-2.0" 27 | 28 | [[annotations]] 29 | path = ".github/ISSUE_TEMPLATE/**" 30 | precedence = "aggregate" 31 | SPDX-FileCopyrightText = "2024 the original author or authors" 32 | SPDX-License-Identifier = "Apache-2.0" 33 | -------------------------------------------------------------------------------- /config/checkstyle/suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 15 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /external-sop/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # External-SOP 8 | 9 | Access an external SOP binary from within your Java/Kotlin application. 10 | 11 | This module implements a backend for `sop-java` that binds to external SOP binaries (such as 12 | [sqop](https://gitlab.com/sequoia-pgp/sequoia-sop/), [python-sop](https://pypi.org/project/sop/) etc.). 13 | SOP operation calls will be delegated to the external binary, and the results are parsed back, so that you can 14 | access them from your Java application as usual. 15 | 16 | ## Example 17 | Let's say you are using `ExampleSOP` which is a binary installed in `/usr/bin/example-sop`. 18 | Instantiating a `SOP` object is as simple as this: 19 | 20 | ```java 21 | SOP sop = new ExternalSOP("/usr/bin/example-sop"); 22 | ``` 23 | 24 | This SOP object can now be used as usual (see [here](../sop-java/README.md)). 25 | 26 | Keep in mind the license of the external SOP binary when integrating one with your project! 27 | 28 | Some SOP binaries might require additional configuration, e.g. a Java based SOP might need to know which JAVA_HOME to use. 29 | For this purpose, additional environment variables can be passed in using a `Properties` object: 30 | 31 | ```java 32 | Properties properties = new Properties(); 33 | properties.put("JAVA_HOME", "/usr/lib/jvm/[...]"); 34 | SOP sop = new ExternalSOP("/usr/bin/example-sop", properties); 35 | ``` 36 | 37 | Most results of SOP operations are communicated via standard-out, standard-in. However, some operations rely on 38 | writing results to additional output files. 39 | To handle such results, we need to provide a temporary directory, to which those results can be written by the SOP, 40 | and from which `External-SOP` reads them back. 41 | The default implementation relies on `Files.createTempDirectory()` to provide a temporary directory. 42 | It is however possible to overwrite this behavior, in order to specify a custom, perhaps more private directory: 43 | 44 | ```java 45 | ExternalSOP.TempDirProvider provider = new ExternalSOP.TempDirProvider() { 46 | @Override 47 | public File provideTempDirectory() throws IOException { 48 | File myTempDir = new File("/path/to/directory"); 49 | myTempDir.mkdirs(); 50 | return myTempDir; 51 | } 52 | }; 53 | SOP sop = new ExternalSOP("/usr/bin/example-sop", provider); 54 | ``` 55 | 56 | ## Testing 57 | The `external-sop` module comes with a growing test suite, which tests SOP binaries against the expectations of the SOP specification. 58 | To configure one or multiple backends for use with the test suite, just provide a custom `config.json` file in `src/main/resources/sop/external`. 59 | An example configuration file with the required file format is available as `config.json.example`. 60 | -------------------------------------------------------------------------------- /external-sop/build.gradle: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | plugins { 6 | id 'java-library' 7 | } 8 | 9 | group 'org.pgpainless' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" 17 | testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" 18 | testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" 19 | 20 | api project(":sop-java") 21 | api "org.slf4j:slf4j-api:$slf4jVersion" 22 | testImplementation "ch.qos.logback:logback-classic:$logbackVersion" 23 | 24 | // @Nonnull, @Nullable... 25 | implementation "com.google.code.findbugs:jsr305:$jsrVersion" 26 | 27 | // The ExternalTestSubjectFactory reads json config file to find configured SOP binaries... 28 | testImplementation "com.google.code.gson:gson:$gsonVersion" 29 | // ...and extends TestSubjectFactory 30 | testImplementation(project(":sop-java-testfixtures")) 31 | } 32 | 33 | test { 34 | // Inject configured external SOP instances using our custom TestSubjectFactory 35 | environment("test.implementation", "sop.testsuite.external.ExternalSOPInstanceFactory") 36 | 37 | useJUnitPlatform() 38 | 39 | // since we test external backends which we might not control, 40 | // we ignore test failures in this module 41 | ignoreFailures = true 42 | } 43 | 44 | -------------------------------------------------------------------------------- /external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.external 6 | 7 | import java.nio.file.Files 8 | import java.util.* 9 | import sop.SOPV 10 | import sop.external.ExternalSOP.TempDirProvider 11 | import sop.external.operation.DetachedVerifyExternal 12 | import sop.external.operation.InlineVerifyExternal 13 | import sop.external.operation.VersionExternal 14 | import sop.operation.DetachedVerify 15 | import sop.operation.InlineVerify 16 | import sop.operation.Version 17 | 18 | /** 19 | * Implementation of the [SOPV] API subset using an external sopv/sop binary. 20 | * 21 | * Instantiate an [ExternalSOPV] object for the given binary and the given [TempDirProvider] using 22 | * empty environment variables. 23 | * 24 | * @param binaryName name / path of the sopv binary 25 | * @param tempDirProvider custom tempDirProvider 26 | */ 27 | class ExternalSOPV( 28 | private val binaryName: String, 29 | private val properties: Properties = Properties(), 30 | private val tempDirProvider: TempDirProvider = defaultTempDirProvider() 31 | ) : SOPV { 32 | 33 | override fun version(): Version = VersionExternal(binaryName, properties) 34 | 35 | override fun detachedVerify(): DetachedVerify = DetachedVerifyExternal(binaryName, properties) 36 | 37 | override fun inlineVerify(): InlineVerify = 38 | InlineVerifyExternal(binaryName, properties, tempDirProvider) 39 | 40 | companion object { 41 | 42 | /** 43 | * Default implementation of the [TempDirProvider] which stores temporary files in the 44 | * systems temp dir ([Files.createTempDirectory]). 45 | * 46 | * @return default implementation 47 | */ 48 | @JvmStatic 49 | fun defaultTempDirProvider(): TempDirProvider { 50 | return TempDirProvider { Files.createTempDirectory("ext-sopv").toFile() } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.external.operation 6 | 7 | import java.io.InputStream 8 | import java.util.Properties 9 | import sop.Ready 10 | import sop.exception.SOPGPException 11 | import sop.external.ExternalSOP 12 | import sop.operation.Armor 13 | 14 | /** Implementation of the [Armor] operation using an external SOP binary. */ 15 | class ArmorExternal(binary: String, environment: Properties) : Armor { 16 | 17 | private val commandList: MutableList = mutableListOf(binary, "armor") 18 | private val envList: List = ExternalSOP.propertiesToEnv(environment) 19 | 20 | @Throws(SOPGPException.BadData::class) 21 | override fun data(data: InputStream): Ready = 22 | ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) 23 | } 24 | -------------------------------------------------------------------------------- /external-sop/src/main/kotlin/sop/external/operation/ChangeKeyPasswordExternal.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.external.operation 6 | 7 | import java.io.InputStream 8 | import java.util.Properties 9 | import sop.Ready 10 | import sop.external.ExternalSOP 11 | import sop.operation.ChangeKeyPassword 12 | 13 | /** Implementation of the [ChangeKeyPassword] operation using an external SOP binary. */ 14 | class ChangeKeyPasswordExternal(binary: String, environment: Properties) : ChangeKeyPassword { 15 | 16 | private val commandList: MutableList = mutableListOf(binary, "change-key-password") 17 | private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() 18 | 19 | private var keyPasswordCounter = 0 20 | 21 | override fun noArmor(): ChangeKeyPassword = apply { commandList.add("--no-armor") } 22 | 23 | override fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword = apply { 24 | commandList.add("--old-key-password=@ENV:KEY_PASSWORD_$keyPasswordCounter") 25 | envList.add("KEY_PASSWORD_$keyPasswordCounter=$oldPassphrase") 26 | keyPasswordCounter += 1 27 | } 28 | 29 | override fun newKeyPassphrase(newPassphrase: String): ChangeKeyPassword = apply { 30 | commandList.add("--new-key-password=@ENV:KEY_PASSWORD_$keyPasswordCounter") 31 | envList.add("KEY_PASSWORD_$keyPasswordCounter=$newPassphrase") 32 | keyPasswordCounter += 1 33 | } 34 | 35 | override fun keys(keys: InputStream): Ready = 36 | ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys) 37 | } 38 | -------------------------------------------------------------------------------- /external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.external.operation 6 | 7 | import java.io.InputStream 8 | import java.util.Properties 9 | import sop.Ready 10 | import sop.external.ExternalSOP 11 | import sop.operation.Dearmor 12 | 13 | /** Implementation of the [Dearmor] operation using an external SOP binary. */ 14 | class DearmorExternal(binary: String, environment: Properties) : Dearmor { 15 | private val commandList = listOf(binary, "dearmor") 16 | private val envList = ExternalSOP.propertiesToEnv(environment) 17 | 18 | override fun data(data: InputStream): Ready = 19 | ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) 20 | } 21 | -------------------------------------------------------------------------------- /external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.external.operation 6 | 7 | import java.io.BufferedReader 8 | import java.io.IOException 9 | import java.io.InputStream 10 | import java.io.InputStreamReader 11 | import java.util.* 12 | import sop.Verification 13 | import sop.Verification.Companion.fromString 14 | import sop.exception.SOPGPException 15 | import sop.external.ExternalSOP 16 | import sop.external.ExternalSOP.Companion.finish 17 | import sop.operation.DetachedVerify 18 | import sop.operation.VerifySignatures 19 | import sop.util.UTCUtil 20 | 21 | /** Implementation of the [DetachedVerify] operation using an external SOP binary. */ 22 | class DetachedVerifyExternal(binary: String, environment: Properties) : DetachedVerify { 23 | 24 | private val commandList = mutableListOf(binary, "verify") 25 | private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() 26 | 27 | private var signatures: InputStream? = null 28 | private val certs: MutableSet = mutableSetOf() 29 | private var argCounter = 0 30 | 31 | override fun signatures(signatures: InputStream): VerifySignatures = apply { 32 | this.signatures = signatures 33 | } 34 | 35 | override fun notBefore(timestamp: Date): DetachedVerify = apply { 36 | commandList.add("--not-before=${UTCUtil.formatUTCDate(timestamp)}") 37 | } 38 | 39 | override fun notAfter(timestamp: Date): DetachedVerify = apply { 40 | commandList.add("--not-after=${UTCUtil.formatUTCDate(timestamp)}") 41 | } 42 | 43 | override fun cert(cert: InputStream): DetachedVerify = apply { this.certs.add(cert) } 44 | 45 | override fun data(data: InputStream): List { 46 | // Signature 47 | if (signatures == null) { 48 | throw SOPGPException.MissingArg("Missing argument: signatures cannot be null.") 49 | } 50 | commandList.add("@ENV:SIGNATURE") 51 | envList.add("SIGNATURE=${ExternalSOP.readString(signatures!!)}") 52 | 53 | // Certs 54 | for (cert in certs) { 55 | commandList.add("@ENV:CERT_$argCounter") 56 | envList.add("CERT_$argCounter=${ExternalSOP.readString(cert)}") 57 | argCounter += 1 58 | } 59 | 60 | try { 61 | val process = 62 | Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) 63 | val processOut = process.outputStream 64 | val processIn = process.inputStream 65 | 66 | val buf = ByteArray(4096) 67 | var r: Int 68 | while (data.read(buf).also { r = it } > 0) { 69 | processOut.write(buf, 0, r) 70 | } 71 | 72 | data.close() 73 | processOut.close() 74 | 75 | val bufferedReader = BufferedReader(InputStreamReader(processIn)) 76 | val verifications: MutableList = ArrayList() 77 | 78 | var line: String? 79 | while (bufferedReader.readLine().also { line = it } != null) { 80 | verifications.add(fromString(line!!)) 81 | } 82 | 83 | finish(process) 84 | 85 | return verifications 86 | } catch (e: IOException) { 87 | throw RuntimeException(e) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /external-sop/src/main/kotlin/sop/external/operation/ExtractCertExternal.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.external.operation 6 | 7 | import java.io.InputStream 8 | import java.util.Properties 9 | import sop.Ready 10 | import sop.external.ExternalSOP 11 | import sop.operation.ExtractCert 12 | 13 | /** Implementation of the [ExtractCert] operation using an external SOP binary. */ 14 | class ExtractCertExternal(binary: String, environment: Properties) : ExtractCert { 15 | 16 | private val commandList = mutableListOf(binary, "extract-cert") 17 | private val envList = ExternalSOP.propertiesToEnv(environment) 18 | 19 | override fun noArmor(): ExtractCert = apply { commandList.add("--no-armor") } 20 | 21 | override fun key(keyInputStream: InputStream): Ready = 22 | ExternalSOP.executeTransformingOperation( 23 | Runtime.getRuntime(), commandList, envList, keyInputStream) 24 | } 25 | -------------------------------------------------------------------------------- /external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.external.operation 6 | 7 | import java.util.Properties 8 | import sop.Ready 9 | import sop.external.ExternalSOP 10 | import sop.operation.GenerateKey 11 | 12 | /** Implementation of the [GenerateKey] operation using an external SOP binary. */ 13 | class GenerateKeyExternal(binary: String, environment: Properties) : GenerateKey { 14 | 15 | private val commandList = mutableListOf(binary, "generate-key") 16 | private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() 17 | 18 | private var argCounter = 0 19 | 20 | override fun noArmor(): GenerateKey = apply { commandList.add("--no-armor") } 21 | 22 | override fun userId(userId: String): GenerateKey = apply { commandList.add(userId) } 23 | 24 | override fun withKeyPassword(password: String): GenerateKey = apply { 25 | commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCounter") 26 | envList.add("KEY_PASSWORD_$argCounter=$password") 27 | argCounter += 1 28 | } 29 | 30 | override fun profile(profile: String): GenerateKey = apply { 31 | commandList.add("--profile=$profile") 32 | } 33 | 34 | override fun signingOnly(): GenerateKey = apply { commandList.add("--signing-only") } 35 | 36 | override fun generate(): Ready = 37 | ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList) 38 | } 39 | -------------------------------------------------------------------------------- /external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.external.operation 6 | 7 | import java.io.* 8 | import java.util.* 9 | import sop.ReadyWithResult 10 | import sop.Signatures 11 | import sop.external.ExternalSOP 12 | import sop.external.ExternalSOP.Companion.finish 13 | import sop.operation.InlineDetach 14 | 15 | /** Implementation of the [InlineDetach] operation using an external SOP binary. */ 16 | class InlineDetachExternal( 17 | binary: String, 18 | environment: Properties, 19 | private val tempDirProvider: ExternalSOP.TempDirProvider 20 | ) : InlineDetach { 21 | 22 | private val commandList = mutableListOf(binary, "inline-detach") 23 | private val envList = ExternalSOP.propertiesToEnv(environment) 24 | 25 | override fun noArmor(): InlineDetach = apply { commandList.add("--no-armor") } 26 | 27 | override fun message(messageInputStream: InputStream): ReadyWithResult { 28 | val tempDir = tempDirProvider.provideTempDirectory() 29 | 30 | val signaturesOut = File(tempDir, "signatures") 31 | signaturesOut.delete() 32 | commandList.add("--signatures-out=${signaturesOut.absolutePath}") 33 | 34 | try { 35 | val process = 36 | Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) 37 | val processOut = process.outputStream 38 | val processIn = process.inputStream 39 | 40 | return object : ReadyWithResult() { 41 | override fun writeTo(outputStream: OutputStream): Signatures { 42 | val buf = ByteArray(4096) 43 | var r: Int 44 | while (messageInputStream.read(buf).also { r = it } > 0) { 45 | processOut.write(buf, 0, r) 46 | } 47 | 48 | messageInputStream.close() 49 | processOut.close() 50 | 51 | while (processIn.read(buf).also { r = it } > 0) { 52 | outputStream.write(buf, 0, r) 53 | } 54 | 55 | processIn.close() 56 | outputStream.close() 57 | 58 | finish(process) 59 | 60 | val signaturesOutIn = FileInputStream(signaturesOut) 61 | val signaturesBuffer = ByteArrayOutputStream() 62 | while (signaturesOutIn.read(buf).also { r = it } > 0) { 63 | signaturesBuffer.write(buf, 0, r) 64 | } 65 | signaturesOutIn.close() 66 | signaturesOut.delete() 67 | 68 | val sigBytes = signaturesBuffer.toByteArray() 69 | 70 | return object : Signatures() { 71 | @Throws(IOException::class) 72 | override fun writeTo(outputStream: OutputStream) { 73 | outputStream.write(sigBytes) 74 | } 75 | } 76 | } 77 | } 78 | } catch (e: IOException) { 79 | throw RuntimeException(e) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.external.operation 6 | 7 | import java.io.InputStream 8 | import java.util.Properties 9 | import sop.Ready 10 | import sop.enums.InlineSignAs 11 | import sop.external.ExternalSOP 12 | import sop.operation.InlineSign 13 | 14 | /** Implementation of the [InlineSign] operation using an external SOP binary. */ 15 | class InlineSignExternal(binary: String, environment: Properties) : InlineSign { 16 | 17 | private val commandList = mutableListOf(binary, "inline-sign") 18 | private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() 19 | 20 | private var argCounter = 0 21 | 22 | override fun mode(mode: InlineSignAs): InlineSign = apply { commandList.add("--as=$mode") } 23 | 24 | override fun data(data: InputStream): Ready = 25 | ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) 26 | 27 | override fun noArmor(): InlineSign = apply { commandList.add("--no-armor") } 28 | 29 | override fun key(key: InputStream): InlineSign = apply { 30 | commandList.add("@ENV:KEY_$argCounter") 31 | envList.add("KEY_$argCounter=${ExternalSOP.readString(key)}") 32 | argCounter += 1 33 | } 34 | 35 | override fun withKeyPassword(password: ByteArray): InlineSign = apply { 36 | commandList.add("--with-key-password=@ENV:WITH_KEY_PASSWORD_$argCounter") 37 | envList.add("WITH_KEY_PASSWORD_$argCounter=${String(password)}") 38 | argCounter += 1 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.external.operation 6 | 7 | import java.io.* 8 | import java.util.* 9 | import sop.ReadyWithResult 10 | import sop.Verification 11 | import sop.Verification.Companion.fromString 12 | import sop.external.ExternalSOP 13 | import sop.external.ExternalSOP.Companion.finish 14 | import sop.operation.InlineVerify 15 | import sop.util.UTCUtil 16 | 17 | /** Implementation of the [InlineVerify] operation using an external SOP binary. */ 18 | class InlineVerifyExternal( 19 | binary: String, 20 | environment: Properties, 21 | private val tempDirProvider: ExternalSOP.TempDirProvider 22 | ) : InlineVerify { 23 | 24 | private val commandList = mutableListOf(binary, "inline-verify") 25 | private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() 26 | 27 | private var argCounter = 0 28 | 29 | override fun data(data: InputStream): ReadyWithResult> { 30 | val tempDir = tempDirProvider.provideTempDirectory() 31 | 32 | val verificationsOut = File(tempDir, "verifications-out") 33 | verificationsOut.delete() 34 | commandList.add("--verifications-out=${verificationsOut.absolutePath}") 35 | 36 | try { 37 | val process = 38 | Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) 39 | val processOut = process.outputStream 40 | val processIn = process.inputStream 41 | 42 | return object : ReadyWithResult>() { 43 | override fun writeTo(outputStream: OutputStream): List { 44 | val buf = ByteArray(4096) 45 | var r: Int 46 | while (data.read(buf).also { r = it } > 0) { 47 | processOut.write(buf, 0, r) 48 | } 49 | 50 | data.close() 51 | processOut.close() 52 | 53 | while (processIn.read(buf).also { r = it } > 0) { 54 | outputStream.write(buf, 0, r) 55 | } 56 | 57 | processIn.close() 58 | outputStream.close() 59 | 60 | finish(process) 61 | 62 | val verificationsOutIn = FileInputStream(verificationsOut) 63 | val reader = BufferedReader(InputStreamReader(verificationsOutIn)) 64 | val verificationList: MutableList = mutableListOf() 65 | var line: String? 66 | while (reader.readLine().also { line = it } != null) { 67 | verificationList.add(fromString(line!!.trim())) 68 | } 69 | 70 | return verificationList 71 | } 72 | } 73 | } catch (e: IOException) { 74 | throw RuntimeException(e) 75 | } 76 | } 77 | 78 | override fun notBefore(timestamp: Date): InlineVerify = apply { 79 | commandList.add("--not-before=${UTCUtil.formatUTCDate(timestamp)}") 80 | } 81 | 82 | override fun notAfter(timestamp: Date): InlineVerify = apply { 83 | commandList.add("--not-after=${UTCUtil.formatUTCDate(timestamp)}") 84 | } 85 | 86 | override fun cert(cert: InputStream): InlineVerify = apply { 87 | commandList.add("@ENV:CERT_$argCounter") 88 | envList.add("CERT_$argCounter=${ExternalSOP.readString(cert)}") 89 | argCounter += 1 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.external.operation 6 | 7 | import java.io.IOException 8 | import java.util.Properties 9 | import sop.Profile 10 | import sop.external.ExternalSOP 11 | import sop.operation.ListProfiles 12 | 13 | /** Implementation of the [ListProfiles] operation using an external SOP binary. */ 14 | class ListProfilesExternal(binary: String, environment: Properties) : ListProfiles { 15 | 16 | private val commandList = mutableListOf(binary, "list-profiles") 17 | private val envList = ExternalSOP.propertiesToEnv(environment) 18 | 19 | override fun subcommand(command: String): List { 20 | return try { 21 | String( 22 | ExternalSOP.executeProducingOperation( 23 | Runtime.getRuntime(), commandList.plus(command), envList) 24 | .bytes) 25 | .let { toProfiles(it) } 26 | } catch (e: IOException) { 27 | throw RuntimeException(e) 28 | } 29 | } 30 | 31 | companion object { 32 | @JvmStatic 33 | private fun toProfiles(output: String): List = 34 | output.split("\n").filter { it.isNotBlank() }.map { Profile.parse(it) } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /external-sop/src/main/kotlin/sop/external/operation/RevokeKeyExternal.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.external.operation 6 | 7 | import java.io.InputStream 8 | import java.util.Properties 9 | import sop.Ready 10 | import sop.external.ExternalSOP 11 | import sop.operation.RevokeKey 12 | 13 | /** Implementation of the [RevokeKey] operation using an external SOP binary. */ 14 | class RevokeKeyExternal(binary: String, environment: Properties) : RevokeKey { 15 | 16 | private val commandList = mutableListOf(binary, "revoke-key") 17 | private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() 18 | 19 | private var argCount = 0 20 | 21 | override fun noArmor(): RevokeKey = apply { commandList.add("--no-armor") } 22 | 23 | override fun withKeyPassword(password: ByteArray): RevokeKey = apply { 24 | commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount") 25 | envList.add("KEY_PASSWORD_$argCount=${String(password)}") 26 | argCount += 1 27 | } 28 | 29 | override fun keys(keys: InputStream): Ready = 30 | ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys) 31 | } 32 | -------------------------------------------------------------------------------- /external-sop/src/main/resources/sop/testsuite/external/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | config.json -------------------------------------------------------------------------------- /external-sop/src/main/resources/sop/testsuite/external/config.json.ci: -------------------------------------------------------------------------------- 1 | { 2 | "backends": [ 3 | { 4 | "name": "Sequoia-SOP", 5 | "sop": "/usr/bin/sqop" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /external-sop/src/main/resources/sop/testsuite/external/config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "backends": [ 3 | { 4 | "name": "Example-SOP", 5 | "sop": "/usr/bin/example-sop" 6 | }, 7 | { 8 | "name": "Awesome-SOP", 9 | "sop": "/usr/local/bin/awesome-sop", 10 | "env": [ 11 | { 12 | "key": "myEnvironmentVariable", "value": "FooBar" 13 | } 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /external-sop/src/test/java/sop/testsuite/external/ExternalSOPInstanceFactory.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.external; 6 | 7 | import com.google.gson.Gson; 8 | import sop.SOP; 9 | import sop.external.ExternalSOP; 10 | import sop.testsuite.SOPInstanceFactory; 11 | 12 | import java.io.File; 13 | import java.io.InputStream; 14 | import java.io.InputStreamReader; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Properties; 19 | 20 | /** 21 | * This implementation of {@link SOPInstanceFactory} reads the JSON file at 22 | *
external-sop/src/main/resources/sop/testsuite/external/config.json
23 | * to determine configured external test backends. 24 | */ 25 | public class ExternalSOPInstanceFactory extends SOPInstanceFactory { 26 | 27 | @Override 28 | public Map provideSOPInstances() { 29 | Map backends = new HashMap<>(); 30 | TestSuite suite = readConfiguration(); 31 | if (suite != null && !suite.backends.isEmpty()) { 32 | for (TestSubject subject : suite.backends) { 33 | if (!new File(subject.sop).exists()) { 34 | continue; 35 | } 36 | 37 | Properties env = new Properties(); 38 | if (subject.env != null) { 39 | for (Var var : subject.env) { 40 | env.put(var.key, var.value); 41 | } 42 | } 43 | 44 | SOP sop = new ExternalSOP(subject.sop, env); 45 | backends.put(subject.name, sop); 46 | } 47 | } 48 | return backends; 49 | } 50 | 51 | 52 | public static TestSuite readConfiguration() { 53 | Gson gson = new Gson(); 54 | InputStream inputStream = ExternalSOPInstanceFactory.class.getResourceAsStream("config.json"); 55 | if (inputStream == null) { 56 | return null; 57 | } 58 | 59 | InputStreamReader reader = new InputStreamReader(inputStream); 60 | return gson.fromJson(reader, TestSuite.class); 61 | } 62 | 63 | 64 | // JSON DTOs 65 | 66 | public static class TestSuite { 67 | List backends; 68 | } 69 | 70 | public static class TestSubject { 71 | String name; 72 | String sop; 73 | List env; 74 | } 75 | 76 | public static class Var { 77 | String key; 78 | String value; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /external-sop/src/test/java/sop/testsuite/external/operation/ExternalArmorDearmorTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.external.operation; 6 | 7 | import org.junit.jupiter.api.condition.EnabledIf; 8 | import sop.testsuite.operation.ArmorDearmorTest; 9 | 10 | @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") 11 | public class ExternalArmorDearmorTest extends ArmorDearmorTest { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /external-sop/src/test/java/sop/testsuite/external/operation/ExternalDecryptWithSessionKeyTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.external.operation; 6 | 7 | import org.junit.jupiter.api.condition.EnabledIf; 8 | import sop.testsuite.operation.DecryptWithSessionKeyTest; 9 | 10 | @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") 11 | public class ExternalDecryptWithSessionKeyTest extends DecryptWithSessionKeyTest { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /external-sop/src/test/java/sop/testsuite/external/operation/ExternalDetachedSignDetachedVerifyTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.external.operation; 6 | 7 | import org.junit.jupiter.api.condition.EnabledIf; 8 | import sop.testsuite.operation.DetachedSignDetachedVerifyTest; 9 | 10 | @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") 11 | public class ExternalDetachedSignDetachedVerifyTest extends DetachedSignDetachedVerifyTest { 12 | } 13 | -------------------------------------------------------------------------------- /external-sop/src/test/java/sop/testsuite/external/operation/ExternalEncryptDecryptTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.external.operation; 6 | 7 | import org.junit.jupiter.api.condition.EnabledIf; 8 | import sop.testsuite.operation.EncryptDecryptTest; 9 | 10 | @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") 11 | public class ExternalEncryptDecryptTest extends EncryptDecryptTest { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /external-sop/src/test/java/sop/testsuite/external/operation/ExternalExtractCertTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.external.operation; 6 | 7 | import org.junit.jupiter.api.condition.EnabledIf; 8 | import sop.testsuite.operation.ExtractCertTest; 9 | 10 | @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") 11 | public class ExternalExtractCertTest extends ExtractCertTest { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /external-sop/src/test/java/sop/testsuite/external/operation/ExternalGenerateKeyTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.external.operation; 6 | 7 | import org.junit.jupiter.api.condition.EnabledIf; 8 | import sop.testsuite.operation.GenerateKeyTest; 9 | 10 | @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") 11 | public class ExternalGenerateKeyTest extends GenerateKeyTest { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineDetachDetachedVerifyTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.external.operation; 6 | 7 | import org.junit.jupiter.api.condition.EnabledIf; 8 | import sop.testsuite.operation.InlineSignInlineDetachDetachedVerifyTest; 9 | 10 | @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") 11 | public class ExternalInlineSignInlineDetachDetachedVerifyTest 12 | extends InlineSignInlineDetachDetachedVerifyTest { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineVerifyTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.external.operation; 6 | 7 | import org.junit.jupiter.api.condition.EnabledIf; 8 | import sop.testsuite.operation.InlineSignInlineVerifyTest; 9 | 10 | @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") 11 | public class ExternalInlineSignInlineVerifyTest extends InlineSignInlineVerifyTest { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /external-sop/src/test/java/sop/testsuite/external/operation/ExternalListProfilesTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.external.operation; 6 | 7 | import org.junit.jupiter.api.condition.EnabledIf; 8 | import sop.testsuite.operation.ListProfilesTest; 9 | 10 | @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") 11 | public class ExternalListProfilesTest extends ListProfilesTest { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.external.operation; 6 | 7 | import org.junit.jupiter.api.condition.EnabledIf; 8 | import sop.testsuite.operation.RevokeKeyTest; 9 | 10 | @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") 11 | public class ExternalRevokeKeyTest extends RevokeKeyTest { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /external-sop/src/test/java/sop/testsuite/external/operation/ExternalVersionTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.external.operation; 6 | 7 | import org.junit.jupiter.api.condition.EnabledIf; 8 | import sop.testsuite.operation.VersionTest; 9 | 10 | @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") 11 | public class ExternalVersionTest extends VersionTest { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: CC0-1.0 4 | 5 | rootProject.name = 'SOP-Java' 6 | 7 | include 'sop-java', 8 | 'sop-java-picocli', 9 | 'sop-java-testfixtures', 10 | 'external-sop' 11 | 12 | -------------------------------------------------------------------------------- /sop-java-picocli/README.md: -------------------------------------------------------------------------------- 1 | 6 | # SOP-Java-Picocli 7 | 8 | [![javadoc](https://javadoc.io/badge2/org.pgpainless/sop-java-picocli/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/sop-java-picocli) 9 | [![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/sop-java-picocli)](https://search.maven.org/artifact/org.pgpainless/sop-java-picocli) 10 | 11 | Implementation of the [Stateless OpenPGP Command Line Interface](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) specification. 12 | This terminal application allows generation of OpenPGP keys, extraction of public key certificates, 13 | armoring and de-armoring of data, as well as - of course - encryption/decryption of messages and creation/verification of signatures. 14 | 15 | ## Install a SOP backend 16 | 17 | This module comes without a SOP backend, so in order to function you need to extend it with an implementation of the interfaces defined in `sop-java`. 18 | An implementation using PGPainless can be found in the module `pgpainless-sop`, but it is of course possible to provide your 19 | own implementation. 20 | 21 | Just install your SOP backend by calling 22 | ```java 23 | // static method call prior to execution of the main method 24 | SopCLI.setSopInstance(yourSopImpl); 25 | ``` 26 | 27 | ## Usage 28 | 29 | To get an overview of available commands of the application, execute 30 | ```shell 31 | java -jar sop-java-picocli-XXX.jar help 32 | ``` 33 | 34 | If you just want to get started encrypting messages, see the module `pgpainless-cli` which initializes 35 | `sop-java-picocli` with `pgpainless-sop`, so you can get started right away without the need to manually wire stuff up. 36 | 37 | Enjoy! 38 | -------------------------------------------------------------------------------- /sop-java-picocli/build.gradle: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | plugins { 6 | id 'application' 7 | id 'org.asciidoctor.jvm.convert' version '3.3.2' 8 | } 9 | 10 | dependencies { 11 | // JUnit 12 | testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" 13 | testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" 14 | 15 | // Mocking Components 16 | testImplementation "org.mockito:mockito-core:$mockitoVersion" 17 | 18 | // SOP 19 | implementation(project(":sop-java")) 20 | testImplementation(project(":sop-java-testfixtures")) 21 | 22 | // CLI 23 | implementation "info.picocli:picocli:$picocliVersion" 24 | kapt "info.picocli:picocli-codegen:$picocliVersion" 25 | 26 | // @Nonnull, @Nullable... 27 | implementation "com.google.code.findbugs:jsr305:$jsrVersion" 28 | } 29 | 30 | mainClassName = 'sop.cli.picocli.SopCLI' 31 | 32 | application { 33 | mainClass = mainClassName 34 | } 35 | 36 | compileJava { 37 | options.compilerArgs += ["-Aproject=${project.group}/${project.name}"] 38 | } 39 | 40 | jar { 41 | dependsOn(":sop-java:jar") 42 | duplicatesStrategy(DuplicatesStrategy.EXCLUDE) 43 | 44 | manifest { 45 | attributes 'Main-Class': "$mainClassName" 46 | } 47 | 48 | from { 49 | configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } 50 | } { 51 | exclude "META-INF/*.SF" 52 | exclude "META-INF/*.DSA" 53 | exclude "META-INF/*.RSA" 54 | } 55 | } 56 | 57 | task generateManpageAsciiDoc(type: JavaExec) { 58 | dependsOn(classes) 59 | group = "Documentation" 60 | description = "Generate AsciiDoc manpage" 61 | classpath(configurations.annotationProcessor, sourceSets.main.runtimeClasspath) 62 | systemProperty("user.language", "en") 63 | main 'picocli.codegen.docgen.manpage.ManPageGenerator' 64 | args mainClassName, "--outdir=${project.buildDir}/generated-picocli-docs", "-v" //, "--template-dir=src/docs/mantemplates" 65 | } 66 | 67 | apply plugin: 'org.asciidoctor.jvm.convert' 68 | asciidoctor { 69 | attributes 'reproducible': '' 70 | dependsOn(generateManpageAsciiDoc) 71 | sourceDir = file("${project.buildDir}/generated-picocli-docs") 72 | outputDir = file("${project.buildDir}/docs") 73 | logDocuments = true 74 | outputOptions { 75 | backends = ['manpage', 'html5'] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli 6 | 7 | import picocli.CommandLine.* 8 | import sop.exception.SOPGPException 9 | 10 | class SOPExceptionExitCodeMapper : IExitCodeExceptionMapper { 11 | 12 | override fun getExitCode(exception: Throwable): Int = 13 | if (exception is SOPGPException) { 14 | // SOPGPExceptions have well-defined exit code 15 | exception.getExitCode() 16 | } else if (exception is UnmatchedArgumentException) { 17 | if (exception.isUnknownOption) { 18 | // Unmatched option of subcommand (e.g. `generate-key --unknown`) 19 | SOPGPException.UnsupportedOption.EXIT_CODE 20 | } else { 21 | // Unmatched subcommand 22 | SOPGPException.UnsupportedSubcommand.EXIT_CODE 23 | } 24 | } else if (exception is MissingParameterException) { 25 | SOPGPException.MissingArg.EXIT_CODE 26 | } else if (exception is ParameterException) { 27 | // Invalid option (e.g. `--as invalid`) 28 | SOPGPException.UnsupportedOption.EXIT_CODE 29 | } else { 30 | // Others, like IOException etc. 31 | 1 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExecutionExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli 6 | 7 | import picocli.CommandLine 8 | import picocli.CommandLine.IExecutionExceptionHandler 9 | 10 | class SOPExecutionExceptionHandler : IExecutionExceptionHandler { 11 | override fun handleExecutionException( 12 | ex: Exception, 13 | commandLine: CommandLine, 14 | parseResult: CommandLine.ParseResult 15 | ): Int { 16 | val exitCode = 17 | if (commandLine.exitCodeExceptionMapper != null) 18 | commandLine.exitCodeExceptionMapper.getExitCode(ex) 19 | else commandLine.commandSpec.exitCodeOnExecutionException() 20 | 21 | val colorScheme = commandLine.colorScheme 22 | if (ex.message != null) { 23 | commandLine.getErr().println(colorScheme.errorText(ex.message)) 24 | } else { 25 | commandLine.getErr().println(ex.javaClass.getName()) 26 | } 27 | 28 | if (SopCLI.stacktrace) { 29 | ex.printStackTrace(commandLine.getErr()) 30 | } 31 | 32 | return exitCode 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands 6 | 7 | import java.io.IOException 8 | import picocli.CommandLine.Command 9 | import sop.cli.picocli.SopCLI 10 | import sop.exception.SOPGPException.BadData 11 | import sop.exception.SOPGPException.UnsupportedOption 12 | 13 | @Command( 14 | name = "armor", 15 | resourceBundle = "msg_armor", 16 | exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) 17 | class ArmorCmd : AbstractSopCmd() { 18 | 19 | override fun run() { 20 | val armor = throwIfUnsupportedSubcommand(SopCLI.getSop().armor(), "armor") 21 | 22 | try { 23 | val ready = armor.data(System.`in`) 24 | ready.writeTo(System.out) 25 | } catch (badData: BadData) { 26 | val errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data") 27 | throw BadData(errorMsg, badData) 28 | } catch (e: IOException) { 29 | throw RuntimeException(e) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands 6 | 7 | import java.io.IOException 8 | import java.lang.RuntimeException 9 | import picocli.CommandLine.Command 10 | import picocli.CommandLine.Option 11 | import sop.cli.picocli.SopCLI 12 | import sop.exception.SOPGPException 13 | 14 | @Command( 15 | name = "change-key-password", 16 | resourceBundle = "msg_change-key-password", 17 | exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) 18 | class ChangeKeyPasswordCmd : AbstractSopCmd() { 19 | 20 | @Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true 21 | 22 | @Option(names = ["--old-key-password"], paramLabel = "PASSWORD") 23 | var oldKeyPasswords: List = listOf() 24 | 25 | @Option(names = ["--new-key-password"], arity = "0..1", paramLabel = "PASSWORD") 26 | var newKeyPassword: String? = null 27 | 28 | override fun run() { 29 | val changeKeyPassword = 30 | throwIfUnsupportedSubcommand(SopCLI.getSop().changeKeyPassword(), "change-key-password") 31 | 32 | if (!armor) { 33 | changeKeyPassword.noArmor() 34 | } 35 | 36 | oldKeyPasswords.forEach { 37 | val password = stringFromInputStream(getInput(it)) 38 | changeKeyPassword.oldKeyPassphrase(password) 39 | } 40 | 41 | newKeyPassword?.let { 42 | val password = stringFromInputStream(getInput(it)) 43 | changeKeyPassword.newKeyPassphrase(password) 44 | } 45 | 46 | try { 47 | changeKeyPassword.keys(System.`in`).writeTo(System.out) 48 | } catch (e: IOException) { 49 | throw RuntimeException(e) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands 6 | 7 | import java.io.IOException 8 | import picocli.CommandLine.Command 9 | import sop.cli.picocli.SopCLI 10 | import sop.exception.SOPGPException 11 | import sop.exception.SOPGPException.BadData 12 | 13 | @Command( 14 | name = "dearmor", 15 | resourceBundle = "msg_dearmor", 16 | exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) 17 | class DearmorCmd : AbstractSopCmd() { 18 | 19 | override fun run() { 20 | val dearmor = throwIfUnsupportedSubcommand(SopCLI.getSop().dearmor(), "dearmor") 21 | 22 | try { 23 | dearmor.data(System.`in`).writeTo(System.out) 24 | } catch (badData: BadData) { 25 | val errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data") 26 | throw BadData(errorMsg, badData) 27 | } catch (e: IOException) { 28 | e.message?.let { 29 | val errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data") 30 | if (it == "invalid armor" || 31 | it == "invalid armor header" || 32 | it == "inconsistent line endings in headers" || 33 | it.startsWith("unable to decode base64 data")) { 34 | throw BadData(errorMsg, e) 35 | } 36 | throw RuntimeException(e) 37 | } 38 | ?: throw RuntimeException(e) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ExtractCertCmd.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands 6 | 7 | import java.io.IOException 8 | import picocli.CommandLine.Command 9 | import picocli.CommandLine.Option 10 | import sop.cli.picocli.SopCLI 11 | import sop.exception.SOPGPException 12 | import sop.exception.SOPGPException.BadData 13 | 14 | @Command( 15 | name = "extract-cert", 16 | resourceBundle = "msg_extract-cert", 17 | exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) 18 | class ExtractCertCmd : AbstractSopCmd() { 19 | 20 | @Option(names = ["--no-armor"], negatable = true) var armor = true 21 | 22 | override fun run() { 23 | val extractCert = 24 | throwIfUnsupportedSubcommand(SopCLI.getSop().extractCert(), "extract-cert") 25 | 26 | if (!armor) { 27 | extractCert.noArmor() 28 | } 29 | 30 | try { 31 | val ready = extractCert.key(System.`in`) 32 | ready.writeTo(System.out) 33 | } catch (e: IOException) { 34 | throw RuntimeException(e) 35 | } catch (badData: BadData) { 36 | val errorMsg = getMsg("sop.error.input.stdin_not_a_private_key") 37 | throw BadData(errorMsg, badData) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands 6 | 7 | import java.io.IOException 8 | import picocli.CommandLine.* 9 | import sop.cli.picocli.SopCLI 10 | import sop.exception.SOPGPException.UnsupportedOption 11 | import sop.exception.SOPGPException.UnsupportedProfile 12 | 13 | @Command( 14 | name = "generate-key", 15 | resourceBundle = "msg_generate-key", 16 | exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) 17 | class GenerateKeyCmd : AbstractSopCmd() { 18 | 19 | @Option(names = ["--no-armor"], negatable = true) var armor = true 20 | 21 | @Parameters(paramLabel = "USERID") var userId: List = listOf() 22 | 23 | @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") 24 | var withKeyPassword: String? = null 25 | 26 | @Option(names = ["--profile"], paramLabel = "PROFILE") var profile: String? = null 27 | 28 | @Option(names = ["--signing-only"]) var signingOnly: Boolean = false 29 | 30 | override fun run() { 31 | val generateKey = 32 | throwIfUnsupportedSubcommand(SopCLI.getSop().generateKey(), "generate-key") 33 | 34 | profile?.let { 35 | try { 36 | generateKey.profile(it) 37 | } catch (e: UnsupportedProfile) { 38 | val errorMsg = 39 | getMsg("sop.error.usage.profile_not_supported", "generate-key", profile!!) 40 | throw UnsupportedProfile(errorMsg, e) 41 | } 42 | } 43 | 44 | if (signingOnly) { 45 | generateKey.signingOnly() 46 | } 47 | 48 | for (userId in userId) { 49 | generateKey.userId(userId) 50 | } 51 | 52 | if (!armor) { 53 | generateKey.noArmor() 54 | } 55 | 56 | withKeyPassword?.let { 57 | try { 58 | val password = stringFromInputStream(getInput(it)) 59 | generateKey.withKeyPassword(password) 60 | } catch (e: UnsupportedOption) { 61 | val errorMsg = 62 | getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") 63 | throw UnsupportedOption(errorMsg, e) 64 | } catch (e: IOException) { 65 | throw RuntimeException(e) 66 | } 67 | } 68 | 69 | try { 70 | val ready = generateKey.generate() 71 | ready.writeTo(System.out) 72 | } catch (e: IOException) { 73 | throw RuntimeException(e) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands 6 | 7 | import java.io.IOException 8 | import java.lang.RuntimeException 9 | import picocli.CommandLine.Command 10 | import picocli.CommandLine.Option 11 | import sop.cli.picocli.SopCLI 12 | import sop.exception.SOPGPException 13 | 14 | @Command( 15 | name = "inline-detach", 16 | resourceBundle = "msg_inline-detach", 17 | exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) 18 | class InlineDetachCmd : AbstractSopCmd() { 19 | 20 | @Option(names = ["--signatures-out"], paramLabel = "SIGNATURES") 21 | var signaturesOut: String? = null 22 | 23 | @Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true 24 | 25 | override fun run() { 26 | val inlineDetach = 27 | throwIfUnsupportedSubcommand(SopCLI.getSop().inlineDetach(), "inline-detach") 28 | 29 | throwIfOutputExists(signaturesOut) 30 | throwIfMissingArg(signaturesOut, "--signatures-out") 31 | 32 | if (!armor) { 33 | inlineDetach.noArmor() 34 | } 35 | 36 | try { 37 | getOutput(signaturesOut).use { sigOut -> 38 | inlineDetach 39 | .message(System.`in`) 40 | .writeTo(System.out) // message out 41 | .writeTo(sigOut) // signatures out 42 | } 43 | } catch (e: IOException) { 44 | throw RuntimeException(e) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands 6 | 7 | import java.io.IOException 8 | import picocli.CommandLine.* 9 | import sop.cli.picocli.SopCLI 10 | import sop.enums.InlineSignAs 11 | import sop.exception.SOPGPException.* 12 | 13 | @Command( 14 | name = "inline-sign", 15 | resourceBundle = "msg_inline-sign", 16 | exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) 17 | class InlineSignCmd : AbstractSopCmd() { 18 | 19 | @Option(names = ["--no-armor"], negatable = true) var armor = true 20 | 21 | @Option(names = ["--as"], paramLabel = "{binary|text|clearsigned}") 22 | var type: InlineSignAs? = null 23 | 24 | @Parameters(paramLabel = "KEYS") var secretKeyFile: List = listOf() 25 | 26 | @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") 27 | var withKeyPassword: List = listOf() 28 | 29 | override fun run() { 30 | val inlineSign = throwIfUnsupportedSubcommand(SopCLI.getSop().inlineSign(), "inline-sign") 31 | 32 | if (!armor && type == InlineSignAs.clearsigned) { 33 | val errorMsg = getMsg("sop.error.usage.incompatible_options.clearsigned_no_armor") 34 | throw IncompatibleOptions(errorMsg) 35 | } 36 | 37 | type?.let { 38 | try { 39 | inlineSign.mode(it) 40 | } catch (unsupportedOption: UnsupportedOption) { 41 | val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as") 42 | throw UnsupportedOption(errorMsg, unsupportedOption) 43 | } 44 | } 45 | 46 | if (secretKeyFile.isEmpty()) { 47 | val errorMsg = getMsg("sop.error.usage.parameter_required", "KEYS") 48 | throw MissingArg(errorMsg) 49 | } 50 | 51 | for (passwordFile in withKeyPassword) { 52 | try { 53 | val password = stringFromInputStream(getInput(passwordFile)) 54 | inlineSign.withKeyPassword(password) 55 | } catch (unsupportedOption: UnsupportedOption) { 56 | val errorMsg = 57 | getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") 58 | throw UnsupportedOption(errorMsg, unsupportedOption) 59 | } catch (e: IOException) { 60 | throw RuntimeException(e) 61 | } 62 | } 63 | 64 | for (keyInput in secretKeyFile) { 65 | try { 66 | getInput(keyInput).use { keyIn -> inlineSign.key(keyIn) } 67 | } catch (e: IOException) { 68 | throw RuntimeException(e) 69 | } catch (e: KeyIsProtected) { 70 | val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput) 71 | throw KeyIsProtected(errorMsg, e) 72 | } catch (badData: BadData) { 73 | val errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput) 74 | throw BadData(errorMsg, badData) 75 | } 76 | } 77 | 78 | if (!armor) { 79 | inlineSign.noArmor() 80 | } 81 | 82 | try { 83 | val ready = inlineSign.data(System.`in`) 84 | ready.writeTo(System.out) 85 | } catch (e: IOException) { 86 | throw RuntimeException(e) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands 6 | 7 | import picocli.CommandLine.Command 8 | import picocli.CommandLine.Parameters 9 | import sop.cli.picocli.SopCLI 10 | import sop.exception.SOPGPException 11 | import sop.exception.SOPGPException.UnsupportedProfile 12 | 13 | @Command( 14 | name = "list-profiles", 15 | resourceBundle = "msg_list-profiles", 16 | exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) 17 | class ListProfilesCmd : AbstractSopCmd() { 18 | 19 | @Parameters(paramLabel = "COMMAND", arity = "1", descriptionKey = "subcommand") 20 | lateinit var subcommand: String 21 | 22 | override fun run() { 23 | val listProfiles = 24 | throwIfUnsupportedSubcommand(SopCLI.getSop().listProfiles(), "list-profiles") 25 | 26 | try { 27 | listProfiles.subcommand(subcommand).forEach { println(it) } 28 | } catch (e: UnsupportedProfile) { 29 | val errorMsg = 30 | getMsg("sop.error.feature_support.subcommand_does_not_support_profiles", subcommand) 31 | throw UnsupportedProfile(errorMsg, e) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands 6 | 7 | import java.io.IOException 8 | import picocli.CommandLine.Command 9 | import picocli.CommandLine.Option 10 | import sop.cli.picocli.SopCLI 11 | import sop.exception.SOPGPException 12 | import sop.exception.SOPGPException.KeyIsProtected 13 | 14 | @Command( 15 | name = "revoke-key", 16 | resourceBundle = "msg_revoke-key", 17 | exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) 18 | class RevokeKeyCmd : AbstractSopCmd() { 19 | 20 | @Option(names = ["--no-armor"], negatable = true) var armor = true 21 | 22 | @Option(names = ["--with-key-password"], paramLabel = "PASSWORD", arity = "0..*") 23 | var withKeyPassword: List = listOf() 24 | 25 | override fun run() { 26 | val revokeKey = throwIfUnsupportedSubcommand(SopCLI.getSop().revokeKey(), "revoke-key") 27 | 28 | if (!armor) { 29 | revokeKey.noArmor() 30 | } 31 | 32 | for (passwordIn in withKeyPassword) { 33 | try { 34 | val password = stringFromInputStream(getInput(passwordIn)) 35 | revokeKey.withKeyPassword(password) 36 | } catch (e: SOPGPException.UnsupportedOption) { 37 | val errorMsg = 38 | getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") 39 | throw SOPGPException.UnsupportedOption(errorMsg, e) 40 | } catch (e: IOException) { 41 | throw RuntimeException(e) 42 | } 43 | } 44 | 45 | val ready = 46 | try { 47 | revokeKey.keys(System.`in`) 48 | } catch (e: KeyIsProtected) { 49 | val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", "STANDARD_IN") 50 | throw KeyIsProtected(errorMsg, e) 51 | } 52 | try { 53 | ready.writeTo(System.out) 54 | } catch (e: IOException) { 55 | throw RuntimeException(e) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/SignCmd.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands 6 | 7 | import java.io.IOException 8 | import picocli.CommandLine.* 9 | import sop.cli.picocli.SopCLI 10 | import sop.enums.SignAs 11 | import sop.exception.SOPGPException 12 | import sop.exception.SOPGPException.BadData 13 | import sop.exception.SOPGPException.KeyIsProtected 14 | 15 | @Command( 16 | name = "sign", 17 | resourceBundle = "msg_detached-sign", 18 | exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) 19 | class SignCmd : AbstractSopCmd() { 20 | 21 | @Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true 22 | 23 | @Option(names = ["--as"], paramLabel = "{binary|text}") var type: SignAs? = null 24 | 25 | @Parameters(paramLabel = "KEYS") var secretKeyFile: List = listOf() 26 | 27 | @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") 28 | var withKeyPassword: List = listOf() 29 | 30 | @Option(names = ["--micalg-out"], paramLabel = "MICALG") var micAlgOut: String? = null 31 | 32 | override fun run() { 33 | val detachedSign = throwIfUnsupportedSubcommand(SopCLI.getSop().detachedSign(), "sign") 34 | 35 | throwIfOutputExists(micAlgOut) 36 | throwIfEmptyParameters(secretKeyFile, "KEYS") 37 | 38 | try { 39 | type?.let { detachedSign.mode(it) } 40 | } catch (unsupported: SOPGPException.UnsupportedOption) { 41 | val errorMsg = 42 | getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") 43 | throw SOPGPException.UnsupportedOption(errorMsg, unsupported) 44 | } catch (ioe: IOException) { 45 | throw RuntimeException(ioe) 46 | } 47 | 48 | withKeyPassword.forEach { passIn -> 49 | try { 50 | val password = stringFromInputStream(getInput(passIn)) 51 | detachedSign.withKeyPassword(password) 52 | } catch (unsupported: SOPGPException.UnsupportedOption) { 53 | val errorMsg = 54 | getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") 55 | throw SOPGPException.UnsupportedOption(errorMsg, unsupported) 56 | } catch (e: IOException) { 57 | throw RuntimeException(e) 58 | } 59 | } 60 | 61 | secretKeyFile.forEach { keyIn -> 62 | try { 63 | getInput(keyIn).use { input -> detachedSign.key(input) } 64 | } catch (ioe: IOException) { 65 | throw RuntimeException(ioe) 66 | } catch (keyIsProtected: KeyIsProtected) { 67 | val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyIn) 68 | throw KeyIsProtected(errorMsg, keyIsProtected) 69 | } catch (badData: BadData) { 70 | val errorMsg = getMsg("sop.error.input.not_a_private_key", keyIn) 71 | throw BadData(errorMsg, badData) 72 | } 73 | } 74 | 75 | if (!armor) { 76 | detachedSign.noArmor() 77 | } 78 | 79 | try { 80 | val ready = detachedSign.data(System.`in`) 81 | val result = ready.writeTo(System.out) 82 | 83 | if (micAlgOut != null) { 84 | getOutput(micAlgOut).use { result.micAlg.writeTo(it) } 85 | } 86 | } catch (e: IOException) { 87 | throw java.lang.RuntimeException(e) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VerifyCmd.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands 6 | 7 | import java.io.IOException 8 | import picocli.CommandLine.* 9 | import sop.cli.picocli.SopCLI 10 | import sop.exception.SOPGPException.* 11 | 12 | @Command( 13 | name = "verify", 14 | resourceBundle = "msg_detached-verify", 15 | exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) 16 | class VerifyCmd : AbstractSopCmd() { 17 | 18 | @Parameters(index = "0", paramLabel = "SIGNATURE") lateinit var signature: String 19 | 20 | @Parameters(index = "1..*", arity = "1..*", paramLabel = "CERT") 21 | lateinit var certificates: List 22 | 23 | @Option(names = ["--not-before"], paramLabel = "DATE") var notBefore: String = "-" 24 | 25 | @Option(names = ["--not-after"], paramLabel = "DATE") var notAfter: String = "now" 26 | 27 | override fun run() { 28 | val detachedVerify = 29 | throwIfUnsupportedSubcommand(SopCLI.getSop().detachedVerify(), "verify") 30 | try { 31 | detachedVerify.notAfter(parseNotAfter(notAfter)) 32 | } catch (unsupportedOption: UnsupportedOption) { 33 | val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after") 34 | throw UnsupportedOption(errorMsg, unsupportedOption) 35 | } 36 | 37 | try { 38 | detachedVerify.notBefore(parseNotBefore(notBefore)) 39 | } catch (unsupportedOption: UnsupportedOption) { 40 | val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before") 41 | throw UnsupportedOption(errorMsg, unsupportedOption) 42 | } 43 | 44 | for (certInput in certificates) { 45 | try { 46 | getInput(certInput).use { certIn -> detachedVerify.cert(certIn) } 47 | } catch (ioException: IOException) { 48 | throw RuntimeException(ioException) 49 | } catch (badData: BadData) { 50 | val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput) 51 | throw BadData(errorMsg, badData) 52 | } 53 | } 54 | 55 | try { 56 | getInput(signature).use { sigIn -> detachedVerify.signatures(sigIn) } 57 | } catch (e: IOException) { 58 | throw RuntimeException(e) 59 | } catch (badData: BadData) { 60 | val errorMsg = getMsg("sop.error.input.not_a_signature", signature) 61 | throw BadData(errorMsg, badData) 62 | } 63 | 64 | val verifications = 65 | try { 66 | detachedVerify.data(System.`in`) 67 | } catch (e: NoSignature) { 68 | val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found") 69 | throw NoSignature(errorMsg, e) 70 | } catch (ioException: IOException) { 71 | throw RuntimeException(ioException) 72 | } catch (badData: BadData) { 73 | val errorMsg = getMsg("sop.error.input.stdin_not_a_message") 74 | throw BadData(errorMsg, badData) 75 | } 76 | 77 | for (verification in verifications) { 78 | println(verification.toString()) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands 6 | 7 | import picocli.CommandLine.ArgGroup 8 | import picocli.CommandLine.Command 9 | import picocli.CommandLine.Option 10 | import sop.cli.picocli.SopCLI 11 | import sop.exception.SOPGPException 12 | 13 | @Command( 14 | name = "version", 15 | resourceBundle = "msg_version", 16 | exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) 17 | class VersionCmd : AbstractSopCmd() { 18 | 19 | @ArgGroup var exclusive: Exclusive? = null 20 | 21 | class Exclusive { 22 | @Option(names = ["--extended"]) var extended: Boolean = false 23 | @Option(names = ["--backend"]) var backend: Boolean = false 24 | @Option(names = ["--sop-spec"]) var sopSpec: Boolean = false 25 | @Option(names = ["--sopv"]) var sopv: Boolean = false 26 | } 27 | 28 | override fun run() { 29 | val version = throwIfUnsupportedSubcommand(SopCLI.getSop().version(), "version") 30 | 31 | if (exclusive == null) { 32 | // No option provided 33 | println("${version.getName()} ${version.getVersion()}") 34 | return 35 | } 36 | 37 | if (exclusive!!.extended) { 38 | println(version.getExtendedVersion()) 39 | return 40 | } 41 | 42 | if (exclusive!!.backend) { 43 | println(version.getBackendVersion()) 44 | return 45 | } 46 | 47 | if (exclusive!!.sopSpec) { 48 | println(version.getSopSpecVersion()) 49 | return 50 | } 51 | 52 | if (exclusive!!.sopv) { 53 | println(version.getSopVVersion()) 54 | return 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_armor.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Add ASCII Armor to standard input 5 | 6 | stacktrace=Print stacktrace 7 | # Generic TODO: Remove when bumping picocli to 4.7.0 8 | usage.synopsisHeading=Usage:\u0020 9 | usage.commandListHeading = %nCommands:%n 10 | usage.optionListHeading = %nOptions:%n 11 | usage.footerHeading=Powered by picocli%n 12 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_armor_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_armor_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_change-key-password.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Update the password of a key 5 | usage.description.0=Unlock all secret keys from STDIN using the given old passwords and emit them re-locked using the new password to STDOUT. 6 | usage.description.1=If any (sub-) key cannot be unlocked, this operation will exit with error code 67. 7 | no-armor=ASCII armor the output 8 | new-key-password.0=New password to lock the keys with. 9 | new-key-password.1=If no new password is passed in, the keys will be emitted unlocked. 10 | new-key-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). 11 | old-key-password.0=Old passwords to unlock the keys with. 12 | old-key-password.1=Multiple passwords can be passed in, which are tested sequentially to unlock locked subkeys. 13 | old-key-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). 14 | 15 | stacktrace=Print stacktrace 16 | # Generic TODO: Remove when bumping picocli to 4.7.0 17 | usage.descriptionHeading=%nDescription:%n 18 | usage.synopsisHeading=Usage:\u0020 19 | usage.commandListHeading = %nCommands:%n 20 | usage.optionListHeading = %nOptions:%n 21 | usage.footerHeading=Powered by picocli%n 22 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_change-key-password_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_dearmor.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Remove ASCII Armor from standard input 5 | 6 | stacktrace=Print stacktrace 7 | # Generic TODO: Remove when bumping picocli to 4.7.0 8 | usage.synopsisHeading=Usage:\u0020 9 | usage.commandListHeading = %nCommands:%n 10 | usage.optionListHeading = %nOptions:%n 11 | usage.footerHeading=Powered by picocli%n 12 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_dearmor_de.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Entferne ASCII Armor von Standard-Eingabe 5 | 6 | stacktrace=Stacktrace ausgeben 7 | # Generic TODO: Remove when bumping picocli to 4.7.0 8 | usage.synopsisHeading=Aufruf:\u0020 9 | usage.commandListHeading=%nBefehle:%n 10 | usage.optionListHeading = %nOptionen:%n 11 | usage.footerHeading=Powered by Picocli%n 12 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_decrypt.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Decrypt a message 5 | session-key-out=Can be used to learn the session key on successful decryption 6 | with-session-key.0=Symmetric message key (session key). 7 | with-session-key.1=Enables decryption of the "CIPHERTEXT" using the session key directly against the "SEIPD" packet. 8 | with-session-key.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). 9 | with-password.0=Symmetric passphrase to decrypt the message with. 10 | with-password.1=Enables decryption based on any "SKESK" packets in the "CIPHERTEXT". 11 | with-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). 12 | verify-out=Emits signature verification status to the designated output 13 | verify-with=Certificates for signature verification 14 | verify-not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) 15 | verify-not-before.1=Reject signatures with a creation date not in range. 16 | verify-not-before.2=Defaults to beginning of time ('-'). 17 | verify-not-after.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) 18 | verify-not-after.1=Reject signatures with a creation date not in range. 19 | verify-not-after.2=Defaults to current system time ('now'). 20 | verify-not-after.3=Accepts special value '-' for end of time. 21 | with-key-password.0=Passphrase to unlock the secret key(s). 22 | with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). 23 | KEY[0..*]=Secret keys to attempt decryption with 24 | 25 | stacktrace=Print stacktrace 26 | # Generic TODO: Remove when bumping picocli to 4.7.0 27 | usage.parameterListHeading=%nParameters:%n 28 | usage.synopsisHeading=Usage:\u0020 29 | usage.commandListHeading = %nCommands:%n 30 | usage.optionListHeading = %nOptions:%n 31 | usage.footerHeading=Powered by picocli%n 32 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_decrypt_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_decrypt_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_detached-sign.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Create a detached message signature 5 | no-armor=ASCII armor the output 6 | as.0=Specify the output format of the signed message. 7 | as.1=Defaults to 'binary'. 8 | as.2=If '--as=text' and the input data is not valid UTF-8, sign fails with return code 53. 9 | with-key-password.0=Passphrase to unlock the secret key(s). 10 | with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). 11 | micalg-out=Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156). 12 | KEYS[0..*]=Secret keys used for signing 13 | 14 | stacktrace=Print stacktrace 15 | # Generic TODO: Remove when bumping picocli to 4.7.0 16 | usage.parameterListHeading=%nParameters:%n 17 | usage.synopsisHeading=Usage:\u0020 18 | usage.commandListHeading = %nCommands:%n 19 | usage.optionListHeading = %nOptions:%n 20 | usage.footerHeading=Powered by picocli%n 21 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_detached-sign_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_detached-sign_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_detached-verify.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Verify a detached signature 5 | usage.description=Verify a detached signature over some data from STDIN. 6 | not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) 7 | not-before.1=Reject signatures with a creation date not in range. 8 | not-before.2=Defaults to beginning of time ("-"). 9 | not-after.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) 10 | not-after.1=Reject signatures with a creation date not in range. 11 | not-after.2=Defaults to current system time ("now"). 12 | not-after.3=Accepts special value "-" for end of time. 13 | SIGNATURE[0]=Detached signature 14 | CERT[1..*]=Public key certificates for signature verification 15 | 16 | stacktrace=Print stacktrace 17 | # Generic TODO: Remove when bumping picocli to 4.7.0 18 | usage.descriptionHeading=%nDescription:%n 19 | usage.parameterListHeading=%nParameters:%n 20 | usage.synopsisHeading=Usage:\u0020 21 | usage.commandListHeading = %nCommands:%n 22 | usage.optionListHeading = %nOptions:%n 23 | usage.footerHeading=Powered by picocli%n 24 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_detached-verify_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_detached-verify_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_encrypt.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Encrypt a message from standard input 5 | no-armor=ASCII armor the output 6 | as=Type of the input data. Defaults to 'binary' 7 | with-password.0=Encrypt the message with a password. 8 | with-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). 9 | sign-with=Sign the output with a private key 10 | profile=Profile identifier to switch between profiles 11 | with-key-password.0=Passphrase to unlock the secret key(s). 12 | with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). 13 | CERTS[0..*]=Certificates the message gets encrypted to 14 | 15 | stacktrace=Print stacktrace 16 | # Generic TODO: Remove when bumping picocli to 4.7.0 17 | usage.parameterListHeading=%nParameters:%n 18 | usage.synopsisHeading=Usage:\u0020 19 | usage.commandListHeading = %nCommands:%n 20 | usage.optionListHeading = %nOptions:%n 21 | usage.footerHeading=Powered by picocli%n 22 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_encrypt_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_encrypt_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_extract-cert.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Extract a public key certificate from a secret key 5 | usage.description=Read a secret key from STDIN and emit the public key certificate to STDOUT. 6 | no-armor=ASCII armor the output 7 | 8 | stacktrace=Print stacktrace 9 | # Generic TODO: Remove when bumping picocli to 4.7.0 10 | usage.descriptionHeading=%nDescription:%n 11 | usage.synopsisHeading=Usage:\u0020 12 | usage.commandListHeading = %nCommands:%n 13 | usage.optionListHeading = %nOptions:%n 14 | usage.footerHeading=Powered by picocli%n 15 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_extract-cert_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_extract-cert_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_generate-key.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Generate a secret key 5 | no-armor=ASCII armor the output 6 | USERID[0..*]=User-ID, e.g. "Alice " 7 | profile=Profile identifier to switch between profiles 8 | signing-only=Generate a key that can only be used for signing 9 | with-key-password.0=Password to protect the private key with 10 | with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). 11 | 12 | stacktrace=Print stacktrace 13 | # Generic TODO: Remove when bumping picocli to 4.7.0 14 | usage.parameterListHeading=%nParameters:%n 15 | usage.synopsisHeading=Usage:\u0020 16 | usage.commandListHeading = %nCommands:%n 17 | usage.optionListHeading = %nOptions:%n 18 | usage.footerHeading=Powered by picocli%n 19 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_generate-key_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_generate-key_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_help.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Display usage information for the specified subcommand 5 | 6 | stacktrace=Print stacktrace 7 | # Generic TODO: Remove when bumping picocli to 4.7.0 8 | usage.synopsisHeading=Usage:\u0020 9 | usage.commandListHeading = %nCommands:%n 10 | usage.optionListHeading = %nOptions:%n 11 | usage.footerHeading=Powered by picocli%n 12 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_help_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_help_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_inline-detach.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Split signatures from a clearsigned message 5 | no-armor=ASCII armor the output 6 | signatures-out=Destination to which a detached signatures block will be written 7 | 8 | stacktrace=Print stacktrace 9 | # Generic TODO: Remove when bumping picocli to 4.7.0 10 | usage.synopsisHeading=Usage:\u0020 11 | usage.commandListHeading = %nCommands:%n 12 | usage.optionListHeading = %nOptions:%n 13 | usage.footerHeading=Powered by picocli%n 14 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_inline-detach_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_inline-detach_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_inline-sign.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Create an inline-signed message 5 | no-armor=ASCII armor the output 6 | as.0=Specify the signature format of the signed message. 7 | as.1='text' and 'binary' will produce inline-signed messages. 8 | as.2='clearsigned' will make use of the cleartext signature framework. 9 | as.3=Defaults to 'binary'. 10 | as.4=If '--as=text' and the input data is not valid UTF-8, inline-sign fails with return code 53. 11 | with-key-password.0=Passphrase to unlock the secret key(s). 12 | with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). 13 | micalg=Emits the digest algorithm used to the specified file in a way that can be used to populate the micalg parameter for the PGP/MIME Content-Type (RFC3156). 14 | KEYS[0..*]=Secret keys used for signing 15 | 16 | stacktrace=Print stacktrace 17 | # Generic TODO: Remove when bumping picocli to 4.7.0 18 | usage.parameterListHeading=%nParameters:%n 19 | usage.synopsisHeading=Usage:\u0020 20 | usage.commandListHeading = %nCommands:%n 21 | usage.optionListHeading = %nOptions:%n 22 | usage.footerHeading=Powered by picocli%n 23 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_inline-sign_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_inline-sign_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_inline-verify.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Verify an inline-signed message 5 | not-before.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) 6 | not-before.1=Reject signatures with a creation date not in range. 7 | not-before.2=Defaults to beginning of time ("-"). 8 | not-after.0=ISO-8601 formatted UTC date (e.g. '2020-11-23T16:35Z) 9 | not-after.1=Reject signatures with a creation date not in range. 10 | not-after.2=Defaults to current system time ("now"). 11 | not-after.3=Accepts special value "-" for end of time. 12 | verifications-out=File to write details over successful verifications to 13 | CERT[0..*]=Public key certificates for signature verification 14 | 15 | stacktrace=Print stacktrace 16 | # Generic TODO: Remove when bumping picocli to 4.7.0 17 | usage.parameterListHeading=%nParameters:%n 18 | usage.synopsisHeading=Usage:\u0020 19 | usage.commandListHeading = %nCommands:%n 20 | usage.optionListHeading = %nOptions:%n 21 | usage.footerHeading=Powered by picocli%n 22 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_inline-verify_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_inline-verify_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_list-profiles.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Emit a list of profiles supported by the identified subcommand 5 | subcommand=Subcommand for which to list profiles 6 | 7 | stacktrace=Print stacktrace 8 | # Generic TODO: Remove when bumping picocli to 4.7.0 9 | usage.parameterListHeading=%nParameters:%n 10 | usage.synopsisHeading=Usage:\u0020 11 | usage.commandListHeading = %nCommands:%n 12 | usage.optionListHeading = %nOptions:%n 13 | usage.footerHeading=Powered by picocli%n 14 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_list-profiles_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_list-profiles_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_revoke-key.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Generate revocation certificates 5 | usage.description=Emit revocation certificates for secret keys from STDIN to STDOUT. 6 | no-armor=ASCII armor the output 7 | with-key-password.0=Passphrase to unlock the secret key(s). 8 | with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). 9 | 10 | stacktrace=Print stacktrace 11 | # Generic TODO: Remove when bumping picocli to 4.7.0 12 | usage.descriptionHeading=%nDescription:%n 13 | usage.synopsisHeading=Usage:\u0020 14 | usage.commandListHeading = %nCommands:%n 15 | usage.optionListHeading = %nOptions:%n 16 | usage.footerHeading=Powered by picocli%n 17 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_revoke-key_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_sop_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_sop_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_version.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | usage.header=Display version information about the tool 5 | extended=Print an extended version string 6 | backend=Print information about the cryptographic backend 7 | sop-spec=Print the latest revision of the SOP specification targeted by the implementation 8 | 9 | stacktrace=Print stacktrace 10 | # Generic TODO: Remove when bumping picocli to 4.7.0 11 | usage.synopsisHeading=Usage:\u0020 12 | usage.commandListHeading = %nCommands:%n 13 | usage.optionListHeading = %nOptions:%n 14 | usage.footerHeading=Powered by picocli%n 15 | -------------------------------------------------------------------------------- /sop-java-picocli/src/main/resources/msg_version_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgpainless/sop-java/ad137d63514822945cb94a0487f36b0dc5eb91b8/sop-java-picocli/src/main/resources/msg_version_de.properties -------------------------------------------------------------------------------- /sop-java-picocli/src/test/java/sop/cli/picocli/DateParsingTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | import java.util.Date; 10 | 11 | import org.junit.jupiter.api.Test; 12 | import sop.cli.picocli.commands.AbstractSopCmd; 13 | import sop.cli.picocli.commands.ArmorCmd; 14 | import sop.util.UTCUtil; 15 | 16 | public class DateParsingTest { 17 | private final AbstractSopCmd cmd = new ArmorCmd(); // we use ArmorCmd as a concrete implementation. 18 | 19 | @Test 20 | public void parseNotAfterDashReturnsEndOfTime() { 21 | assertEquals(AbstractSopCmd.END_OF_TIME, cmd.parseNotAfter("-")); 22 | } 23 | 24 | @Test 25 | public void parseNotBeforeDashReturnsBeginningOfTime() { 26 | assertEquals(AbstractSopCmd.BEGINNING_OF_TIME, cmd.parseNotBefore("-")); 27 | } 28 | 29 | @Test 30 | public void parseNotAfterNowReturnsNow() { 31 | assertEquals(new Date().getTime(), cmd.parseNotAfter("now").getTime(), 1000); 32 | } 33 | 34 | @Test 35 | public void parseNotBeforeNowReturnsNow() { 36 | assertEquals(new Date().getTime(), cmd.parseNotBefore("now").getTime(), 1000); 37 | } 38 | 39 | @Test 40 | public void parseNotAfterTimestamp() { 41 | String timestamp = "2019-10-24T23:48:29Z"; 42 | Date date = cmd.parseNotAfter(timestamp); 43 | assertEquals(timestamp, UTCUtil.formatUTCDate(date)); 44 | } 45 | 46 | @Test 47 | public void parseNotBeforeTimestamp() { 48 | String timestamp = "2019-10-29T18:36:45Z"; 49 | Date date = cmd.parseNotBefore(timestamp); 50 | assertEquals(timestamp, UTCUtil.formatUTCDate(date)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sop-java-picocli/src/test/java/sop/cli/picocli/TestFileUtil.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli; 6 | 7 | import java.io.File; 8 | import java.io.FileOutputStream; 9 | import java.io.IOException; 10 | import java.nio.charset.StandardCharsets; 11 | import java.nio.file.Files; 12 | 13 | public class TestFileUtil { 14 | 15 | public static File createTempDir() throws IOException { 16 | File tempDir = Files.createTempDirectory("tmpFir").toFile(); 17 | tempDir.deleteOnExit(); 18 | tempDir.mkdirs(); 19 | return tempDir; 20 | } 21 | 22 | public static File writeTempStringFile(String string) throws IOException { 23 | File tempDir = createTempDir(); 24 | 25 | File passwordFile = new File(tempDir, "file"); 26 | passwordFile.createNewFile(); 27 | 28 | FileOutputStream fileOut = new FileOutputStream(passwordFile); 29 | fileOut.write(string.getBytes(StandardCharsets.UTF_8)); 30 | fileOut.close(); 31 | 32 | return passwordFile; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands; 6 | 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import sop.Ready; 10 | import sop.SOP; 11 | import sop.cli.picocli.SopCLI; 12 | import sop.exception.SOPGPException; 13 | import sop.operation.Armor; 14 | 15 | import javax.annotation.Nonnull; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.io.OutputStream; 19 | 20 | import static org.mockito.ArgumentMatchers.any; 21 | import static org.mockito.Mockito.mock; 22 | import static org.mockito.Mockito.times; 23 | import static org.mockito.Mockito.verify; 24 | import static org.mockito.Mockito.when; 25 | import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; 26 | import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; 27 | 28 | public class ArmorCmdTest { 29 | 30 | private Armor armor; 31 | private SOP sop; 32 | 33 | @BeforeEach 34 | public void mockComponents() throws SOPGPException.BadData, IOException { 35 | armor = mock(Armor.class); 36 | sop = mock(SOP.class); 37 | when(sop.armor()).thenReturn(armor); 38 | when(armor.data((InputStream) any())).thenReturn(nopReady()); 39 | 40 | SopCLI.setSopInstance(sop); 41 | } 42 | 43 | @Test 44 | public void assertDataIsAlwaysCalled() throws SOPGPException.BadData, IOException { 45 | assertSuccess(() -> SopCLI.execute("armor")); 46 | verify(armor, times(1)).data((InputStream) any()); 47 | } 48 | 49 | @Test 50 | public void ifBadDataExit41() throws SOPGPException.BadData, IOException { 51 | when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); 52 | 53 | assertBadData(() -> SopCLI.execute("armor")); 54 | } 55 | 56 | @Test 57 | public void ifNoErrorsNoExit() { 58 | when(sop.armor()).thenReturn(armor); 59 | 60 | assertSuccess(() -> SopCLI.execute("armor")); 61 | } 62 | 63 | private static Ready nopReady() { 64 | return new Ready() { 65 | @Override 66 | public void writeTo(@Nonnull OutputStream outputStream) { 67 | } 68 | }; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands; 6 | 7 | import static org.mockito.ArgumentMatchers.any; 8 | import static org.mockito.Mockito.mock; 9 | import static org.mockito.Mockito.times; 10 | import static org.mockito.Mockito.verify; 11 | import static org.mockito.Mockito.when; 12 | import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; 13 | import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; 14 | 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.io.OutputStream; 18 | 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | import sop.Ready; 22 | import sop.SOP; 23 | import sop.cli.picocli.SopCLI; 24 | import sop.exception.SOPGPException; 25 | import sop.operation.Dearmor; 26 | 27 | public class DearmorCmdTest { 28 | 29 | private SOP sop; 30 | private Dearmor dearmor; 31 | 32 | @BeforeEach 33 | public void mockComponents() throws IOException, SOPGPException.BadData { 34 | sop = mock(SOP.class); 35 | dearmor = mock(Dearmor.class); 36 | when(dearmor.data((InputStream) any())).thenReturn(nopReady()); 37 | when(sop.dearmor()).thenReturn(dearmor); 38 | 39 | SopCLI.setSopInstance(sop); 40 | } 41 | 42 | private static Ready nopReady() { 43 | return new Ready() { 44 | @Override 45 | public void writeTo(OutputStream outputStream) { 46 | } 47 | }; 48 | } 49 | 50 | @Test 51 | public void assertDataIsCalled() throws IOException, SOPGPException.BadData { 52 | assertSuccess(() -> SopCLI.execute("dearmor")); 53 | verify(dearmor, times(1)).data((InputStream) any()); 54 | } 55 | 56 | @Test 57 | public void assertBadDataCausesExit41() throws IOException, SOPGPException.BadData { 58 | when(dearmor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException("invalid armor"))); 59 | assertBadData(() -> SopCLI.execute("dearmor")); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands; 6 | 7 | import static org.mockito.ArgumentMatchers.any; 8 | import static org.mockito.Mockito.mock; 9 | import static org.mockito.Mockito.never; 10 | import static org.mockito.Mockito.times; 11 | import static org.mockito.Mockito.verify; 12 | import static org.mockito.Mockito.when; 13 | import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData; 14 | import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError; 15 | import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; 16 | 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.io.OutputStream; 20 | 21 | import org.junit.jupiter.api.BeforeEach; 22 | import org.junit.jupiter.api.Test; 23 | import sop.Ready; 24 | import sop.SOP; 25 | import sop.cli.picocli.SopCLI; 26 | import sop.exception.SOPGPException; 27 | import sop.operation.ExtractCert; 28 | 29 | public class ExtractCertCmdTest { 30 | 31 | ExtractCert extractCert; 32 | 33 | @BeforeEach 34 | public void mockComponents() throws IOException, SOPGPException.BadData { 35 | extractCert = mock(ExtractCert.class); 36 | when(extractCert.key((InputStream) any())).thenReturn(new Ready() { 37 | @Override 38 | public void writeTo(OutputStream outputStream) { 39 | } 40 | }); 41 | 42 | SOP sop = mock(SOP.class); 43 | when(sop.extractCert()).thenReturn(extractCert); 44 | 45 | SopCLI.setSopInstance(sop); 46 | } 47 | 48 | @Test 49 | public void noArmor_notCalledByDefault() { 50 | assertSuccess(() -> 51 | SopCLI.execute("extract-cert")); 52 | verify(extractCert, never()).noArmor(); 53 | } 54 | 55 | @Test 56 | public void noArmor_passedDown() { 57 | assertSuccess(() -> 58 | SopCLI.execute("extract-cert", "--no-armor")); 59 | verify(extractCert, times(1)).noArmor(); 60 | } 61 | 62 | @Test 63 | public void key_ioExceptionCausesGenericError() throws IOException, SOPGPException.BadData { 64 | when(extractCert.key((InputStream) any())).thenReturn(new Ready() { 65 | @Override 66 | public void writeTo(OutputStream outputStream) throws IOException { 67 | throw new IOException(); 68 | } 69 | }); 70 | assertGenericError(() -> 71 | SopCLI.execute("extract-cert")); 72 | } 73 | 74 | @Test 75 | public void key_badDataCausesBadData() throws IOException, SOPGPException.BadData { 76 | when(extractCert.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); 77 | assertBadData(() -> 78 | SopCLI.execute("extract-cert")); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands; 6 | 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import sop.ReadyWithResult; 10 | import sop.SOP; 11 | import sop.Signatures; 12 | import sop.cli.picocli.SopCLI; 13 | import sop.cli.picocli.TestFileUtil; 14 | import sop.exception.SOPGPException; 15 | import sop.operation.InlineDetach; 16 | 17 | import java.io.File; 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.io.OutputStream; 21 | import java.nio.charset.StandardCharsets; 22 | 23 | import static org.mockito.ArgumentMatchers.any; 24 | import static org.mockito.Mockito.mock; 25 | import static org.mockito.Mockito.times; 26 | import static org.mockito.Mockito.verify; 27 | import static org.mockito.Mockito.when; 28 | import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg; 29 | import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; 30 | 31 | public class InlineDetachCmdTest { 32 | 33 | InlineDetach inlineDetach; 34 | 35 | @BeforeEach 36 | public void mockComponents() { 37 | inlineDetach = mock(InlineDetach.class); 38 | 39 | SOP sop = mock(SOP.class); 40 | when(sop.inlineDetach()).thenReturn(inlineDetach); 41 | SopCLI.setSopInstance(sop); 42 | } 43 | 44 | @Test 45 | public void testMissingSignaturesOutResultsInMissingArg() { 46 | assertMissingArg(() -> 47 | SopCLI.execute("inline-detach")); 48 | } 49 | 50 | @Test 51 | public void testNoArmorIsCalled() throws IOException { 52 | // Create temp dir and allocate non-existing tempfile for sigout 53 | File tempDir = TestFileUtil.createTempDir(); 54 | File tempFile = new File(tempDir, "sigs.out"); 55 | tempFile.deleteOnExit(); 56 | 57 | // mock inline-detach 58 | when(inlineDetach.message((InputStream) any())) 59 | .thenReturn(new ReadyWithResult() { 60 | @Override 61 | public Signatures writeTo(OutputStream outputStream) throws SOPGPException.NoSignature { 62 | return new Signatures() { 63 | @Override 64 | public void writeTo(OutputStream signatureOutputStream) throws IOException { 65 | signatureOutputStream.write("Signatures!\n".getBytes(StandardCharsets.UTF_8)); 66 | } 67 | }; 68 | } 69 | }); 70 | 71 | assertSuccess(() -> 72 | SopCLI.execute("inline-detach", "--signatures-out", tempFile.getAbsolutePath(), "--no-armor")); 73 | verify(inlineDetach, times(1)).noArmor(); 74 | verify(inlineDetach, times(1)).message((InputStream) any()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /sop-java-picocli/src/test/java/sop/cli/picocli/commands/TestEnvironmentVariableResolver.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | public class TestEnvironmentVariableResolver implements AbstractSopCmd.EnvironmentVariableResolver { 11 | 12 | private final Map environment = new HashMap<>(); 13 | 14 | public void addEnvironmentVariable(String name, String value) { 15 | this.environment.put(name, value); 16 | } 17 | 18 | @Override 19 | public String resolveEnvironmentVariable(String name) { 20 | return environment.get(name); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.cli.picocli.commands; 6 | 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import sop.SOP; 10 | import sop.cli.picocli.SopCLI; 11 | import sop.operation.Version; 12 | 13 | import static org.mockito.Mockito.mock; 14 | import static org.mockito.Mockito.times; 15 | import static org.mockito.Mockito.verify; 16 | import static org.mockito.Mockito.when; 17 | import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; 18 | import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption; 19 | 20 | public class VersionCmdTest { 21 | 22 | private Version version; 23 | 24 | @BeforeEach 25 | public void mockComponents() { 26 | SOP sop = mock(SOP.class); 27 | version = mock(Version.class); 28 | when(version.getName()).thenReturn("MockSop"); 29 | when(version.getVersion()).thenReturn("1.0"); 30 | when(version.getExtendedVersion()).thenReturn("MockSop Extended Version Information"); 31 | when(version.getBackendVersion()).thenReturn("Foo"); 32 | when(version.getSopSpecVersion()).thenReturn("draft-dkg-openpgp-stateless-cli-XX"); 33 | when(version.getSopVVersion()).thenReturn("1.0"); 34 | when(sop.version()).thenReturn(version); 35 | 36 | SopCLI.setSopInstance(sop); 37 | } 38 | 39 | @Test 40 | public void assertVersionCommandWorks() { 41 | assertSuccess(() -> 42 | SopCLI.execute("version")); 43 | verify(version, times(1)).getVersion(); 44 | verify(version, times(1)).getName(); 45 | } 46 | 47 | @Test 48 | public void assertExtendedVersionCommandWorks() { 49 | assertSuccess(() -> 50 | SopCLI.execute("version", "--extended")); 51 | verify(version, times(1)).getExtendedVersion(); 52 | } 53 | 54 | @Test 55 | public void assertBackendVersionCommandWorks() { 56 | assertSuccess(() -> 57 | SopCLI.execute("version", "--backend")); 58 | verify(version, times(1)).getBackendVersion(); 59 | } 60 | 61 | @Test 62 | public void assertSpecVersionCommandWorks() { 63 | assertSuccess(() -> 64 | SopCLI.execute("version", "--sop-spec")); 65 | } 66 | 67 | @Test 68 | public void assertSOPVVersionCommandWorks() { 69 | assertSuccess(() -> 70 | SopCLI.execute("version", "--sopv")); 71 | } 72 | 73 | @Test 74 | public void assertInvalidOptionResultsInExit37() { 75 | assertUnsupportedOption(() -> 76 | SopCLI.execute("version", "--invalid")); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /sop-java-testfixtures/build.gradle: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | plugins { 6 | id 'java-library' 7 | } 8 | 9 | group 'org.pgpainless' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | implementation(project(":sop-java")) 17 | implementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" 18 | implementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" 19 | runtimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" 20 | 21 | // @Nullable, @Nonnull annotations 22 | implementation "com.google.code.findbugs:jsr305:3.0.2" 23 | 24 | } 25 | -------------------------------------------------------------------------------- /sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOption.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2025 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite; 6 | 7 | import java.lang.annotation.ElementType; 8 | import java.lang.annotation.Inherited; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.RetentionPolicy; 11 | import java.lang.annotation.Target; 12 | 13 | @Target(ElementType.TYPE) 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Inherited 16 | public @interface AbortOnUnsupportedOption { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOptionExtension.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2025 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite; 6 | 7 | import org.junit.jupiter.api.extension.ExtensionContext; 8 | import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; 9 | import sop.exception.SOPGPException; 10 | 11 | import java.lang.annotation.Annotation; 12 | 13 | import static org.junit.jupiter.api.Assumptions.assumeTrue; 14 | 15 | public class AbortOnUnsupportedOptionExtension implements TestExecutionExceptionHandler { 16 | 17 | @Override 18 | public void handleTestExecutionException(ExtensionContext extensionContext, Throwable throwable) throws Throwable { 19 | Class testClass = extensionContext.getRequiredTestClass(); 20 | Annotation annotation = testClass.getAnnotation(AbortOnUnsupportedOption.class); 21 | if (annotation != null && throwable instanceof SOPGPException.UnsupportedOption) { 22 | assumeTrue(false, "Test aborted due to: " + throwable.getMessage()); 23 | } 24 | throw throwable; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sop-java-testfixtures/src/main/java/sop/testsuite/SOPInstanceFactory.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite; 6 | 7 | import sop.SOP; 8 | 9 | import java.util.Map; 10 | 11 | /** 12 | * Factory class to instantiate SOP implementations for testing. 13 | * Overwrite this class and the {@link #provideSOPInstances()} method to return the SOP instances you want 14 | * to test. 15 | * Then, add the following line to your
build.gradle
files
dependencies
section: 16 | *
{@code
17 |  *     testImplementation(testFixtures("org.pgpainless:sop-java:"))
18 |  * }
19 | * To inject the factory class into the test suite, add the following line to your modules
test
task: 20 | *
{@code
21 |  *     environment("test.implementation", "org.example.YourTestSubjectFactory")
22 |  * }
23 | * Next, in your
test
sources, extend all test classes from the
testFixtures
24 | *
sop.operation
package. 25 | * Take a look at the
external-sop
module for an example. 26 | */ 27 | public abstract class SOPInstanceFactory { 28 | 29 | public abstract Map provideSOPInstances(); 30 | } 31 | -------------------------------------------------------------------------------- /sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.assertions; 6 | 7 | import sop.Verification; 8 | import sop.enums.SignatureMode; 9 | import sop.testsuite.JUtils; 10 | 11 | import java.util.Date; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | 15 | public final class VerificationAssert { 16 | 17 | private final Verification verification; 18 | 19 | public static VerificationAssert assertThatVerification(Verification verification) { 20 | return new VerificationAssert(verification); 21 | } 22 | 23 | private VerificationAssert(Verification verification) { 24 | this.verification = verification; 25 | } 26 | 27 | public VerificationAssert issuedBy(String signingKeyFingerprint, String primaryFingerprint) { 28 | return isBySigningKey(signingKeyFingerprint) 29 | .issuedBy(primaryFingerprint); 30 | } 31 | 32 | public VerificationAssert issuedBy(String primaryFingerprint) { 33 | assertEquals(primaryFingerprint, verification.getSigningCertFingerprint()); 34 | return this; 35 | } 36 | 37 | public VerificationAssert isBySigningKey(String signingKeyFingerprint) { 38 | assertEquals(signingKeyFingerprint, verification.getSigningKeyFingerprint()); 39 | return this; 40 | } 41 | 42 | public VerificationAssert isCreatedAt(Date creationDate) { 43 | JUtils.assertDateEquals(creationDate, verification.getCreationTime()); 44 | return this; 45 | } 46 | 47 | public VerificationAssert hasDescription(String description) { 48 | assertEquals(description, verification.getDescription().get()); 49 | return this; 50 | } 51 | 52 | public VerificationAssert hasDescriptionOrNull(String description) { 53 | if (verification.getDescription().isEmpty()) { 54 | return this; 55 | } 56 | 57 | return hasDescription(description); 58 | } 59 | 60 | public VerificationAssert hasMode(SignatureMode mode) { 61 | assertEquals(mode, verification.getSignatureMode().get()); 62 | return this; 63 | } 64 | 65 | public VerificationAssert hasModeOrNull(SignatureMode mode) { 66 | if (verification.getSignatureMode().isEmpty()) { 67 | return this; 68 | } 69 | return hasMode(mode); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationListAssert.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.assertions; 6 | 7 | import sop.Verification; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertFalse; 14 | import static org.junit.jupiter.api.Assertions.assertTrue; 15 | import static org.junit.jupiter.api.Assertions.fail; 16 | 17 | public final class VerificationListAssert { 18 | 19 | private final List verificationList = new ArrayList<>(); 20 | 21 | private VerificationListAssert(List verifications) { 22 | this.verificationList.addAll(verifications); 23 | } 24 | 25 | public static VerificationListAssert assertThatVerificationList(List verifications) { 26 | return new VerificationListAssert(verifications); 27 | } 28 | 29 | public VerificationListAssert isEmpty() { 30 | assertTrue(verificationList.isEmpty()); 31 | return this; 32 | } 33 | 34 | public VerificationListAssert isNotEmpty() { 35 | assertFalse(verificationList.isEmpty()); 36 | return this; 37 | } 38 | 39 | public VerificationListAssert sizeEquals(int size) { 40 | assertEquals(size, verificationList.size()); 41 | return this; 42 | } 43 | 44 | public VerificationAssert hasSingleItem() { 45 | sizeEquals(1); 46 | return VerificationAssert.assertThatVerification(verificationList.get(0)); 47 | } 48 | 49 | public VerificationListAssert containsVerificationByCert(String primaryFingerprint) { 50 | for (Verification verification : verificationList) { 51 | if (primaryFingerprint.equals(verification.getSigningCertFingerprint())) { 52 | return this; 53 | } 54 | } 55 | fail("No verification was issued by certificate " + primaryFingerprint); 56 | return this; 57 | } 58 | 59 | public VerificationListAssert containsVerificationBy(String signingKeyFingerprint, String primaryFingerprint) { 60 | for (Verification verification : verificationList) { 61 | if (primaryFingerprint.equals(verification.getSigningCertFingerprint()) && 62 | signingKeyFingerprint.equals(verification.getSigningKeyFingerprint())) { 63 | return this; 64 | } 65 | } 66 | 67 | fail("No verification was issued by key " + signingKeyFingerprint + " of cert " + primaryFingerprint); 68 | return this; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sop-java-testfixtures/src/main/java/sop/testsuite/assertions/package-info.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | /** 6 | * DSL for assertions on SOP objects. 7 | */ 8 | package sop.testsuite.assertions; 9 | -------------------------------------------------------------------------------- /sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.operation; 6 | 7 | import org.junit.jupiter.api.Named; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.junit.jupiter.params.provider.Arguments; 10 | import sop.SOP; 11 | import sop.testsuite.AbortOnUnsupportedOption; 12 | import sop.testsuite.AbortOnUnsupportedOptionExtension; 13 | import sop.testsuite.SOPInstanceFactory; 14 | 15 | import java.lang.reflect.InvocationTargetException; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.stream.Stream; 20 | 21 | @ExtendWith(AbortOnUnsupportedOptionExtension.class) 22 | @AbortOnUnsupportedOption 23 | public abstract class AbstractSOPTest { 24 | 25 | private static final List backends = new ArrayList<>(); 26 | 27 | static { 28 | initBackends(); 29 | } 30 | 31 | // populate instances list via configured test subject factory 32 | private static void initBackends() { 33 | String factoryName = System.getenv("test.implementation"); 34 | if (factoryName == null) { 35 | return; 36 | } 37 | 38 | SOPInstanceFactory factory; 39 | try { 40 | Class testSubjectFactoryClass = Class.forName(factoryName); 41 | factory = (SOPInstanceFactory) testSubjectFactoryClass 42 | .getDeclaredConstructor().newInstance(); 43 | } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | 44 | InvocationTargetException | NoSuchMethodException e) { 45 | throw new RuntimeException(e); 46 | } 47 | 48 | Map testSubjects = factory.provideSOPInstances(); 49 | for (String key : testSubjects.keySet()) { 50 | backends.add(Arguments.of(Named.of(key, testSubjects.get(key)))); 51 | } 52 | } 53 | 54 | public static Stream provideBackends() { 55 | return backends.stream(); 56 | } 57 | 58 | public static boolean hasBackends() { 59 | return !backends.isEmpty(); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.operation; 6 | 7 | import org.junit.jupiter.api.Assertions; 8 | import org.junit.jupiter.api.condition.EnabledIf; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.Arguments; 11 | import org.junit.jupiter.params.provider.MethodSource; 12 | import sop.ByteArrayAndResult; 13 | import sop.DecryptionResult; 14 | import sop.SOP; 15 | import sop.SessionKey; 16 | import sop.testsuite.TestData; 17 | 18 | import java.io.IOException; 19 | import java.nio.charset.StandardCharsets; 20 | import java.util.stream.Stream; 21 | 22 | import static org.junit.jupiter.api.Assertions.assertEquals; 23 | 24 | @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") 25 | public class DecryptWithSessionKeyTest extends AbstractSOPTest { 26 | 27 | private static final String CIPHERTEXT = "-----BEGIN PGP MESSAGE-----\n" + 28 | "\n" + 29 | "wV4DR2b2udXyHrYSAQdAy+Et2hCh4ubh8KsmM8ctRDN6Pee+UHVVcI6YXpY9S2cw\n" + 30 | "1QEROCgfm6xGb+hgxmoFrWhtZU03Arb27ZmpWA6e6Ha9jFdB4/DDbqbhlVuFOmti\n" + 31 | "0j8BqGjEvEYAon+8F9TwMaDbPjjy9SdgQBorlM88ChIW14KQtpG9FZN+r+xVKPG1\n" + 32 | "8EIOxI4qOZaH3Wejraca31M=\n" + 33 | "=1imC\n" + 34 | "-----END PGP MESSAGE-----\n"; 35 | private static final String SESSION_KEY = "9:ED682800F5FEA829A82E8B7DDF8CE9CF4BF9BB45024B017764462EE53101C36A"; 36 | 37 | static Stream provideInstances() { 38 | return provideBackends(); 39 | } 40 | 41 | @ParameterizedTest 42 | @MethodSource("provideInstances") 43 | public void testDecryptAndExtractSessionKey(SOP sop) throws IOException { 44 | ByteArrayAndResult bytesAndResult = sop.decrypt() 45 | .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) 46 | .ciphertext(CIPHERTEXT.getBytes(StandardCharsets.UTF_8)) 47 | .toByteArrayAndResult(); 48 | 49 | assertEquals(SESSION_KEY, bytesAndResult.getResult().getSessionKey().get().toString()); 50 | 51 | Assertions.assertArrayEquals(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8), bytesAndResult.getBytes()); 52 | } 53 | 54 | @ParameterizedTest 55 | @MethodSource("provideInstances") 56 | public void testDecryptWithSessionKey(SOP sop) throws IOException { 57 | byte[] decrypted = sop.decrypt() 58 | .withSessionKey(SessionKey.fromString(SESSION_KEY)) 59 | .ciphertext(CIPHERTEXT.getBytes(StandardCharsets.UTF_8)) 60 | .toByteArrayAndResult() 61 | .getBytes(); 62 | 63 | Assertions.assertArrayEquals(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8), decrypted); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.operation; 6 | 7 | import org.junit.jupiter.params.ParameterizedTest; 8 | import org.junit.jupiter.params.provider.Arguments; 9 | import org.junit.jupiter.params.provider.MethodSource; 10 | import sop.Profile; 11 | import sop.SOP; 12 | import sop.exception.SOPGPException; 13 | 14 | import java.util.List; 15 | import java.util.stream.Stream; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertFalse; 18 | import static org.junit.jupiter.api.Assertions.assertThrows; 19 | 20 | public class ListProfilesTest extends AbstractSOPTest { 21 | 22 | static Stream provideInstances() { 23 | return provideBackends(); 24 | } 25 | 26 | @ParameterizedTest 27 | @MethodSource("provideInstances") 28 | public void listGenerateKeyProfiles(SOP sop) { 29 | List profiles = sop 30 | .listProfiles() 31 | .generateKey(); 32 | 33 | assertFalse(profiles.isEmpty()); 34 | } 35 | 36 | @ParameterizedTest 37 | @MethodSource("provideInstances") 38 | public void listEncryptProfiles(SOP sop) { 39 | List profiles = sop 40 | .listProfiles() 41 | .encrypt(); 42 | 43 | assertFalse(profiles.isEmpty()); 44 | } 45 | 46 | @ParameterizedTest 47 | @MethodSource("provideInstances") 48 | public void listUnsupportedProfiles(SOP sop) { 49 | assertThrows(SOPGPException.UnsupportedProfile.class, () -> sop 50 | .listProfiles() 51 | .subcommand("invalid")); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.testsuite.operation; 6 | 7 | import org.junit.jupiter.api.condition.EnabledIf; 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.junit.jupiter.params.provider.Arguments; 10 | import org.junit.jupiter.params.provider.MethodSource; 11 | import org.opentest4j.TestAbortedException; 12 | import sop.SOP; 13 | import sop.exception.SOPGPException; 14 | 15 | import java.util.stream.Stream; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertFalse; 18 | import static org.junit.jupiter.api.Assertions.assertNotNull; 19 | import static org.junit.jupiter.api.Assertions.assertTrue; 20 | 21 | @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") 22 | public class VersionTest extends AbstractSOPTest { 23 | 24 | static Stream provideInstances() { 25 | return provideBackends(); 26 | } 27 | 28 | @ParameterizedTest 29 | @MethodSource("provideInstances") 30 | public void versionNameTest(SOP sop) { 31 | String name = sop.version().getName(); 32 | assertNotNull(name); 33 | assertFalse(name.isEmpty()); 34 | } 35 | 36 | @ParameterizedTest 37 | @MethodSource("provideInstances") 38 | public void versionVersionTest(SOP sop) { 39 | String version = sop.version().getVersion(); 40 | assertFalse(version.isEmpty()); 41 | } 42 | 43 | @ParameterizedTest 44 | @MethodSource("provideInstances") 45 | public void backendVersionTest(SOP sop) { 46 | String backend = sop.version().getBackendVersion(); 47 | assertFalse(backend.isEmpty()); 48 | } 49 | 50 | @ParameterizedTest 51 | @MethodSource("provideInstances") 52 | public void extendedVersionTest(SOP sop) { 53 | String extended = sop.version().getExtendedVersion(); 54 | assertFalse(extended.isEmpty()); 55 | } 56 | 57 | @ParameterizedTest 58 | @MethodSource("provideInstances") 59 | public void sopSpecVersionTest(SOP sop) { 60 | try { 61 | sop.version().getSopSpecVersion(); 62 | } catch (RuntimeException e) { 63 | throw new TestAbortedException("SOP backend does not support 'version --sop-spec' yet."); 64 | } 65 | 66 | String sopSpec = sop.version().getSopSpecVersion(); 67 | if (sop.version().isSopSpecImplementationIncomplete()) { 68 | assertTrue(sopSpec.startsWith("~draft-dkg-openpgp-stateless-cli-")); 69 | } else { 70 | assertTrue(sopSpec.startsWith("draft-dkg-openpgp-stateless-cli-")); 71 | } 72 | 73 | int sopRevision = sop.version().getSopSpecRevisionNumber(); 74 | assertTrue(sop.version().getSopSpecRevisionName().endsWith("" + sopRevision)); 75 | } 76 | 77 | @ParameterizedTest 78 | @MethodSource("provideInstances") 79 | public void sopVVersionTest(SOP sop) { 80 | try { 81 | sop.version().getSopVVersion(); 82 | } catch (SOPGPException.UnsupportedOption e) { 83 | throw new TestAbortedException( 84 | "Implementation does (gracefully) not provide coverage for any sopv interface version."); 85 | } catch (RuntimeException e) { 86 | throw new TestAbortedException("Implementation does not provide coverage for any sopv interface version."); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /sop-java-testfixtures/src/main/java/sop/testsuite/operation/package-info.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | /** 6 | * SOP binary test suite. 7 | */ 8 | package sop.testsuite.operation; 9 | -------------------------------------------------------------------------------- /sop-java-testfixtures/src/main/java/sop/testsuite/package-info.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | /** 6 | * SOP binary test suite. 7 | */ 8 | package sop.testsuite; 9 | -------------------------------------------------------------------------------- /sop-java/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # SOP-Java 8 | 9 | [![javadoc](https://javadoc.io/badge2/org.pgpainless/sop-java/javadoc.svg)](https://javadoc.io/doc/org.pgpainless/sop-java) 10 | [![Maven Central](https://badgen.net/maven/v/maven-central/org.pgpainless/sop-java)](https://search.maven.org/artifact/org.pgpainless/sop-java) 11 | 12 | Stateless OpenPGP Protocol for Java. 13 | 14 | This module contains interfaces that model the API described by the 15 | [Stateless OpenPGP Command Line Interface](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) specification. 16 | 17 | This module is not a command line application! For that, see `sop-java-picocli`. 18 | 19 | ## Usage Examples 20 | 21 | The API defined by `sop-java` is super straight forward: 22 | ```java 23 | SOP sop = ... // e.g. new org.pgpainless.sop.SOPImpl(); 24 | 25 | // Generate an OpenPGP key 26 | byte[] key = sop.generateKey() 27 | .userId("Alice ") 28 | .generate() 29 | .getBytes(); 30 | 31 | // Extract the certificate (public key) 32 | byte[] cert = sop.extractCert() 33 | .key(key) 34 | .getBytes(); 35 | 36 | // Encrypt a message 37 | byte[] message = ... 38 | byte[] encrypted = sop.encrypt() 39 | .withCert(cert) 40 | .signWith(key) 41 | .plaintext(message) 42 | .getBytes(); 43 | 44 | // Decrypt a message 45 | ByteArrayAndResult messageAndVerifications = sop.decrypt() 46 | .verifyWith(cert) 47 | .withKey(key) 48 | .ciphertext(encrypted) 49 | .toByteArrayAndResult(); 50 | byte[] decrypted = messageAndVerifications.getBytes(); 51 | // Signature Verifications 52 | DecryptionResult messageInfo = messageAndVerifications.getResult(); 53 | List signatureVerifications = messageInfo.getVerifications(); 54 | ``` 55 | 56 | Furthermore, the API is capable of signing messages and verifying unencrypted signed data, as well as adding and removing ASCII armor. 57 | 58 | ## Why should I use this? 59 | 60 | If you need to use OpenPGP functionality like encrypting/decrypting messages, or creating/verifying 61 | signatures inside your application, you probably don't want to start from scratch and instead reuse some library. 62 | 63 | Instead of locking yourselves in by depending hard on that one library, you can simply depend on the interfaces from 64 | `sop-java` and plug in a library (such as `pgpainless-sop`, `external-sop`) that implements said interfaces. 65 | 66 | That way you don't make yourself dependent from a single OpenPGP library and stay flexible. 67 | Should another library emerge, that better suits your needs (and implements `sop-java`), you can easily switch 68 | by swapping out the dependency with minimal changes to your code. 69 | 70 | ## Why should I *implement* this? 71 | 72 | Did you create an [OpenPGP](https://datatracker.ietf.org/doc/html/rfc4880) implementation that can be used in the Java ecosystem? 73 | By implementing the `sop-java` interface, you can turn your library into a command line interface (see `sop-java-picocli`). 74 | This allows you to plug your library into the [OpenPGP interoperability test suite](https://tests.sequoia-pgp.org/) 75 | of the [Sequoia-PGP](https://sequoia-pgp.org/) project. 76 | -------------------------------------------------------------------------------- /sop-java/build.gradle: -------------------------------------------------------------------------------- 1 | import org.apache.tools.ant.filters.ReplaceTokens 2 | 3 | // SPDX-FileCopyrightText: 2021 Paul Schaub 4 | // 5 | // SPDX-License-Identifier: Apache-2.0 6 | 7 | import org.apache.tools.ant.filters.* 8 | plugins { 9 | id 'java-library' 10 | } 11 | 12 | group 'org.pgpainless' 13 | 14 | repositories { 15 | mavenCentral() 16 | } 17 | 18 | dependencies { 19 | testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" 20 | testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" 21 | testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" 22 | testImplementation(project(":sop-java-testfixtures")) 23 | 24 | // @Nullable, @Nonnull annotations 25 | implementation "com.google.code.findbugs:jsr305:3.0.2" 26 | 27 | } 28 | 29 | processResources { 30 | filter ReplaceTokens, tokens: [ 31 | "project.version": project.version.toString() 32 | ] 33 | } 34 | 35 | test { 36 | useJUnitPlatform() 37 | } 38 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop 6 | 7 | import java.io.InputStream 8 | 9 | /** 10 | * Tuple of a [ByteArray] and associated result object. 11 | * 12 | * @param bytes byte array 13 | * @param result result object 14 | * @param type of result 15 | */ 16 | data class ByteArrayAndResult(val bytes: ByteArray, val result: T) { 17 | 18 | /** 19 | * [InputStream] returning the contents of [bytes]. 20 | * 21 | * @return input stream 22 | */ 23 | val inputStream: InputStream 24 | get() = bytes.inputStream() 25 | 26 | override fun equals(other: Any?): Boolean { 27 | if (this === other) return true 28 | if (javaClass != other?.javaClass) return false 29 | 30 | other as ByteArrayAndResult<*> 31 | 32 | if (!bytes.contentEquals(other.bytes)) return false 33 | if (result != other.result) return false 34 | 35 | return true 36 | } 37 | 38 | override fun hashCode(): Int { 39 | var hashCode = bytes.contentHashCode() 40 | hashCode = 31 * hashCode + (result?.hashCode() ?: 0) 41 | return hashCode 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/DecryptionResult.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop 6 | 7 | import sop.util.Optional 8 | 9 | class DecryptionResult(sessionKey: SessionKey?, val verifications: List) { 10 | val sessionKey: Optional 11 | 12 | init { 13 | this.sessionKey = Optional.ofNullable(sessionKey) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/EncryptionResult.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop 6 | 7 | import sop.util.Optional 8 | 9 | class EncryptionResult(sessionKey: SessionKey?) { 10 | val sessionKey: Optional 11 | 12 | init { 13 | this.sessionKey = Optional.ofNullable(sessionKey) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/MicAlg.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop 6 | 7 | import java.io.OutputStream 8 | import java.io.PrintWriter 9 | 10 | data class MicAlg(val micAlg: String) { 11 | 12 | fun writeTo(outputStream: OutputStream) { 13 | PrintWriter(outputStream).use { it.write(micAlg) } 14 | } 15 | 16 | companion object { 17 | @JvmStatic fun empty() = MicAlg("") 18 | 19 | @JvmStatic 20 | fun fromHashAlgorithmId(id: Int) = 21 | when (id) { 22 | 1 -> "pgp-md5" 23 | 2 -> "pgp-sha1" 24 | 3 -> "pgp-ripemd160" 25 | 8 -> "pgp-sha256" 26 | 9 -> "pgp-sha384" 27 | 10 -> "pgp-sha512" 28 | 11 -> "pgp-sha224" 29 | 12 -> "pgp-sha3-256" 30 | 14 -> "pgp-sha3-512" 31 | else -> throw IllegalArgumentException("Unsupported hash algorithm ID: $id") 32 | }.let { MicAlg(it) } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/Ready.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop 6 | 7 | import java.io.ByteArrayInputStream 8 | import java.io.ByteArrayOutputStream 9 | import java.io.IOException 10 | import java.io.InputStream 11 | import java.io.OutputStream 12 | 13 | /** Abstract class that encapsulates output data, waiting to be consumed. */ 14 | abstract class Ready { 15 | 16 | /** 17 | * Write the data to the provided output stream. 18 | * 19 | * @param outputStream output stream 20 | * @throws IOException in case of an IO error 21 | */ 22 | @Throws(IOException::class) abstract fun writeTo(outputStream: OutputStream) 23 | 24 | /** 25 | * Return the data as a byte array by writing it to a [ByteArrayOutputStream] first and then 26 | * returning the array. 27 | * 28 | * @return data as byte array 29 | * @throws IOException in case of an IO error 30 | */ 31 | val bytes: ByteArray 32 | @Throws(IOException::class) 33 | get() = 34 | ByteArrayOutputStream() 35 | .let { 36 | writeTo(it) 37 | it 38 | } 39 | .toByteArray() 40 | 41 | /** 42 | * Return an input stream containing the data. 43 | * 44 | * @return input stream 45 | * @throws IOException in case of an IO error 46 | */ 47 | val inputStream: InputStream 48 | @Throws(IOException::class) get() = ByteArrayInputStream(bytes) 49 | } 50 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/ReadyWithResult.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop 6 | 7 | import java.io.ByteArrayOutputStream 8 | import java.io.IOException 9 | import java.io.OutputStream 10 | import sop.exception.SOPGPException 11 | 12 | abstract class ReadyWithResult { 13 | 14 | /** 15 | * Write the data e.g. decrypted plaintext to the provided output stream and return the result 16 | * of the processing operation. 17 | * 18 | * @param outputStream output stream 19 | * @return result, eg. signatures 20 | * @throws IOException in case of an IO error 21 | * @throws SOPGPException in case of a SOP protocol error 22 | */ 23 | @Throws(IOException::class, SOPGPException::class) 24 | abstract fun writeTo(outputStream: OutputStream): T 25 | 26 | /** 27 | * Return the data as a [ByteArrayAndResult]. Calling [ByteArrayAndResult.bytes] will give you 28 | * access to the data as byte array, while [ByteArrayAndResult.result] will grant access to the 29 | * appended result. 30 | * 31 | * @return byte array and result 32 | * @throws IOException in case of an IO error 33 | * @throws SOPGPException.NoSignature if there are no valid signatures found 34 | */ 35 | @Throws(IOException::class, SOPGPException::class) 36 | fun toByteArrayAndResult() = 37 | ByteArrayOutputStream().let { 38 | val result = writeTo(it) 39 | ByteArrayAndResult(it.toByteArray(), result) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/SOP.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop 6 | 7 | import sop.operation.Armor 8 | import sop.operation.ChangeKeyPassword 9 | import sop.operation.Dearmor 10 | import sop.operation.Decrypt 11 | import sop.operation.DetachedSign 12 | import sop.operation.Encrypt 13 | import sop.operation.ExtractCert 14 | import sop.operation.GenerateKey 15 | import sop.operation.InlineDetach 16 | import sop.operation.InlineSign 17 | import sop.operation.ListProfiles 18 | import sop.operation.RevokeKey 19 | 20 | /** 21 | * Stateless OpenPGP Interface. This class provides a stateless interface to various OpenPGP related 22 | * operations. Note: Subcommand objects acquired by calling any method of this interface are not 23 | * intended for reuse. If you for example need to generate multiple keys, make a dedicated call to 24 | * [generateKey] once per key generation. 25 | */ 26 | interface SOP : SOPV { 27 | 28 | /** Generate a secret key. */ 29 | fun generateKey(): GenerateKey 30 | 31 | /** Extract a certificate (public key) from a secret key. */ 32 | fun extractCert(): ExtractCert 33 | 34 | /** 35 | * Create detached signatures. If you want to sign a message inline, use [inlineSign] instead. 36 | */ 37 | fun sign(): DetachedSign = detachedSign() 38 | 39 | /** 40 | * Create detached signatures. If you want to sign a message inline, use [inlineSign] instead. 41 | */ 42 | fun detachedSign(): DetachedSign 43 | 44 | /** 45 | * Sign a message using inline signatures. If you need to create detached signatures, use 46 | * [detachedSign] instead. 47 | */ 48 | fun inlineSign(): InlineSign 49 | 50 | /** Detach signatures from an inline signed message. */ 51 | fun inlineDetach(): InlineDetach 52 | 53 | /** Encrypt a message. */ 54 | fun encrypt(): Encrypt 55 | 56 | /** Decrypt a message. */ 57 | fun decrypt(): Decrypt 58 | 59 | /** Convert binary OpenPGP data to ASCII. */ 60 | fun armor(): Armor 61 | 62 | /** Converts ASCII armored OpenPGP data to binary. */ 63 | fun dearmor(): Dearmor 64 | 65 | /** List supported [Profiles][Profile] of a subcommand. */ 66 | fun listProfiles(): ListProfiles 67 | 68 | /** Revoke one or more secret keys. */ 69 | fun revokeKey(): RevokeKey 70 | 71 | /** Update a key's password. */ 72 | fun changeKeyPassword(): ChangeKeyPassword 73 | } 74 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/SOPV.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop 6 | 7 | import sop.operation.DetachedVerify 8 | import sop.operation.InlineVerify 9 | import sop.operation.Version 10 | 11 | /** Subset of [SOP] implementing only OpenPGP signature verification. */ 12 | interface SOPV { 13 | 14 | /** Get information about the implementations name and version. */ 15 | fun version(): Version 16 | 17 | /** 18 | * Verify detached signatures. If you need to verify an inline-signed message, use 19 | * [inlineVerify] instead. 20 | */ 21 | fun verify(): DetachedVerify = detachedVerify() 22 | 23 | /** 24 | * Verify detached signatures. If you need to verify an inline-signed message, use 25 | * [inlineVerify] instead. 26 | */ 27 | fun detachedVerify(): DetachedVerify 28 | 29 | /** 30 | * Verify signatures of an inline-signed message. If you need to verify detached signatures over 31 | * a message, use [detachedVerify] instead. 32 | */ 33 | fun inlineVerify(): InlineVerify 34 | } 35 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/SessionKey.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop 6 | 7 | import sop.util.HexUtil 8 | 9 | /** 10 | * Class representing a symmetric session key. 11 | * 12 | * @param algorithm symmetric key algorithm ID 13 | * @param key [ByteArray] containing the session key 14 | */ 15 | data class SessionKey(val algorithm: Byte, val key: ByteArray) { 16 | 17 | override fun equals(other: Any?): Boolean { 18 | if (this === other) return true 19 | if (javaClass != other?.javaClass) return false 20 | 21 | other as SessionKey 22 | 23 | if (algorithm != other.algorithm) return false 24 | if (!key.contentEquals(other.key)) return false 25 | 26 | return true 27 | } 28 | 29 | override fun hashCode(): Int { 30 | var hashCode = algorithm.toInt() 31 | hashCode = 31 * hashCode + key.contentHashCode() 32 | return hashCode 33 | } 34 | 35 | override fun toString(): String = "$algorithm:${HexUtil.bytesToHex(key)}" 36 | 37 | companion object { 38 | 39 | @JvmStatic private val PATTERN = "^(\\d):([0-9A-F]+)$".toPattern() 40 | 41 | @JvmStatic 42 | fun fromString(string: String): SessionKey { 43 | val matcher = PATTERN.matcher(string.trim().uppercase().replace("\n", "")) 44 | require(matcher.matches()) { "Provided session key does not match expected format." } 45 | return SessionKey(matcher.group(1).toByte(), HexUtil.hexToBytes(matcher.group(2))) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/Signatures.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop 6 | 7 | import java.io.IOException 8 | import java.io.OutputStream 9 | 10 | abstract class Signatures : Ready() { 11 | 12 | /** 13 | * Write OpenPGP signatures to the provided output stream. 14 | * 15 | * @param outputStream signature output stream 16 | * @throws IOException in case of an IO error 17 | */ 18 | @Throws(IOException::class) abstract override fun writeTo(outputStream: OutputStream) 19 | } 20 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/SigningResult.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop 6 | 7 | /** 8 | * This class contains various information about a signed message. 9 | * 10 | * @param micAlg string identifying the digest mechanism used to create the signed message. This is 11 | * useful for setting the `micalg=` parameter for the multipart/signed content-type of a PGP/MIME 12 | * object as described in section 5 of 13 | * [RFC3156](https://www.rfc-editor.org/rfc/rfc3156#section-5). If more than one signature was 14 | * generated and different digest mechanisms were used, the value of the micalg object is an empty 15 | * string. 16 | */ 17 | data class SigningResult(val micAlg: MicAlg) { 18 | 19 | class Builder internal constructor() { 20 | private var micAlg = MicAlg.empty() 21 | 22 | fun setMicAlg(micAlg: MicAlg) = apply { this.micAlg = micAlg } 23 | 24 | fun build() = SigningResult(micAlg) 25 | } 26 | 27 | companion object { 28 | @JvmStatic fun builder() = Builder() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/Verification.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop 6 | 7 | import java.text.ParseException 8 | import java.util.Date 9 | import sop.enums.SignatureMode 10 | import sop.util.Optional 11 | import sop.util.UTCUtil 12 | 13 | data class Verification( 14 | val creationTime: Date, 15 | val signingKeyFingerprint: String, 16 | val signingCertFingerprint: String, 17 | val signatureMode: Optional, 18 | val description: Optional 19 | ) { 20 | @JvmOverloads 21 | constructor( 22 | creationTime: Date, 23 | signingKeyFingerprint: String, 24 | signingCertFingerprint: String, 25 | signatureMode: SignatureMode? = null, 26 | description: String? = null 27 | ) : this( 28 | creationTime, 29 | signingKeyFingerprint, 30 | signingCertFingerprint, 31 | Optional.ofNullable(signatureMode), 32 | Optional.ofNullable(description?.trim())) 33 | 34 | override fun toString(): String = 35 | "${UTCUtil.formatUTCDate(creationTime)} $signingKeyFingerprint $signingCertFingerprint" + 36 | (if (signatureMode.isPresent) " mode:${signatureMode.get()}" else "") + 37 | (if (description.isPresent) " ${description.get()}" else "") 38 | 39 | companion object { 40 | @JvmStatic 41 | fun fromString(string: String): Verification { 42 | val split = string.trim().split(" ") 43 | require(split.size >= 3) { 44 | "Verification must be of the format 'UTC-DATE OpenPGPFingerprint OpenPGPFingerprint [mode] [info]'." 45 | } 46 | if (split.size == 3) { 47 | return Verification(parseUTCDate(split[0]), split[1], split[2]) 48 | } 49 | 50 | var index = 3 51 | val mode = 52 | if (split[3].startsWith("mode:")) { 53 | index += 1 54 | SignatureMode.valueOf(split[3].substring("mode:".length)) 55 | } else null 56 | 57 | val description = split.subList(index, split.size).joinToString(" ").ifBlank { null } 58 | 59 | return Verification( 60 | parseUTCDate(split[0]), 61 | split[1], 62 | split[2], 63 | Optional.ofNullable(mode), 64 | Optional.ofNullable(description)) 65 | } 66 | 67 | @JvmStatic 68 | private fun parseUTCDate(string: String): Date { 69 | return try { 70 | UTCUtil.parseUTCDate(string) 71 | } catch (e: ParseException) { 72 | throw IllegalArgumentException("Malformed UTC timestamp.", e) 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/enums/EncryptAs.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.enums 6 | 7 | enum class EncryptAs { 8 | binary, 9 | text 10 | } 11 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.enums 6 | 7 | enum class InlineSignAs { 8 | 9 | /** Signature is made over the binary message. */ 10 | binary, 11 | 12 | /** Signature is made over the message in text mode. */ 13 | text, 14 | 15 | /** Signature is made using the Cleartext Signature Framework. */ 16 | clearsigned 17 | } 18 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/enums/SignAs.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.enums 6 | 7 | enum class SignAs { 8 | /** Signature is made over the binary message. */ 9 | binary, 10 | /** Signature is made over the message in text mode. */ 11 | text 12 | } 13 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/enums/SignatureMode.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.enums 6 | 7 | /** 8 | * Enum referencing relevant signature types. 9 | * 10 | * @see RFC4880 §5.2.1 - Signature 11 | * Types 12 | */ 13 | enum class SignatureMode { 14 | /** Signature of a binary document (type `0x00`). */ 15 | binary, 16 | /** Signature of a canonical text document (type `0x01`). */ 17 | text 18 | } 19 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/operation/AbstractSign.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.operation 6 | 7 | import java.io.IOException 8 | import java.io.InputStream 9 | import sop.exception.SOPGPException.BadData 10 | import sop.exception.SOPGPException.KeyCannotSign 11 | import sop.exception.SOPGPException.PasswordNotHumanReadable 12 | import sop.exception.SOPGPException.UnsupportedAsymmetricAlgo 13 | import sop.exception.SOPGPException.UnsupportedOption 14 | import sop.util.UTF8Util 15 | 16 | /** 17 | * Interface for signing operations. 18 | * 19 | * @param builder subclass 20 | */ 21 | interface AbstractSign { 22 | 23 | /** 24 | * Disable ASCII armor encoding. 25 | * 26 | * @return builder instance 27 | */ 28 | fun noArmor(): T 29 | 30 | /** 31 | * Add one or more signing keys. 32 | * 33 | * @param key input stream containing encoded keys 34 | * @return builder instance 35 | * @throws KeyCannotSign if the key cannot be used for signing 36 | * @throws BadData if the [InputStream] does not contain an OpenPGP key 37 | * @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm 38 | * @throws IOException in case of an IO error 39 | */ 40 | @Throws( 41 | KeyCannotSign::class, BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) 42 | fun key(key: InputStream): T 43 | 44 | /** 45 | * Add one or more signing keys. 46 | * 47 | * @param key byte array containing encoded keys 48 | * @return builder instance 49 | * @throws KeyCannotSign if the key cannot be used for signing 50 | * @throws BadData if the byte array does not contain an OpenPGP key 51 | * @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm 52 | * @throws IOException in case of an IO error 53 | */ 54 | @Throws( 55 | KeyCannotSign::class, BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) 56 | fun key(key: ByteArray): T = key(key.inputStream()) 57 | 58 | /** 59 | * Provide the password for the secret key used for signing. 60 | * 61 | * @param password password 62 | * @return builder instance 63 | * @throws UnsupportedOption if key passwords are not supported 64 | * @throws PasswordNotHumanReadable if the provided passphrase is not human-readable 65 | */ 66 | @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) 67 | fun withKeyPassword(password: String): T = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) 68 | 69 | /** 70 | * Provide the password for the secret key used for signing. 71 | * 72 | * @param password password 73 | * @return builder instance 74 | * @throws UnsupportedOption if key passwords are not supported 75 | * @throws PasswordNotHumanReadable if the provided passphrase is not human-readable 76 | */ 77 | @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) 78 | fun withKeyPassword(password: ByteArray): T 79 | } 80 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/operation/AbstractVerify.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.operation 6 | 7 | import java.io.IOException 8 | import java.io.InputStream 9 | import java.util.* 10 | import sop.exception.SOPGPException.BadData 11 | import sop.exception.SOPGPException.UnsupportedOption 12 | 13 | /** 14 | * Common API methods shared between verification of inline signatures ([InlineVerify]) and 15 | * verification of detached signatures ([DetachedVerify]). 16 | * 17 | * @param Builder type ([DetachedVerify], [InlineVerify]) 18 | */ 19 | interface AbstractVerify { 20 | 21 | /** 22 | * Makes the SOP implementation consider signatures before this date invalid. 23 | * 24 | * @param timestamp timestamp 25 | * @return builder instance 26 | */ 27 | @Throws(UnsupportedOption::class) fun notBefore(timestamp: Date): T 28 | 29 | /** 30 | * Makes the SOP implementation consider signatures after this date invalid. 31 | * 32 | * @param timestamp timestamp 33 | * @return builder instance 34 | */ 35 | @Throws(UnsupportedOption::class) fun notAfter(timestamp: Date): T 36 | 37 | /** 38 | * Add one or more verification cert. 39 | * 40 | * @param cert input stream containing the encoded certs 41 | * @return builder instance 42 | * @throws BadData if the input stream does not contain an OpenPGP certificate 43 | * @throws IOException in case of an IO error 44 | */ 45 | @Throws(BadData::class, IOException::class) fun cert(cert: InputStream): T 46 | 47 | /** 48 | * Add one or more verification cert. 49 | * 50 | * @param cert byte array containing the encoded certs 51 | * @return builder instance 52 | * @throws BadData if the byte array does not contain an OpenPGP certificate 53 | * @throws IOException in case of an IO error 54 | */ 55 | @Throws(BadData::class, IOException::class) 56 | fun cert(cert: ByteArray): T = cert(cert.inputStream()) 57 | } 58 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/operation/Armor.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.operation 6 | 7 | import java.io.IOException 8 | import java.io.InputStream 9 | import sop.Ready 10 | import sop.exception.SOPGPException.BadData 11 | 12 | interface Armor { 13 | 14 | /** 15 | * Armor the provided data. 16 | * 17 | * @param data input stream of unarmored OpenPGP data 18 | * @return armored data 19 | * @throws BadData if the data appears to be OpenPGP packets, but those are broken 20 | * @throws IOException in case of an IO error 21 | */ 22 | @Throws(BadData::class, IOException::class) fun data(data: InputStream): Ready 23 | 24 | /** 25 | * Armor the provided data. 26 | * 27 | * @param data unarmored OpenPGP data 28 | * @return armored data 29 | * @throws BadData if the data appears to be OpenPGP packets, but those are broken 30 | * @throws IOException in case of an IO error 31 | */ 32 | @Throws(BadData::class, IOException::class) 33 | fun data(data: ByteArray): Ready = data(data.inputStream()) 34 | } 35 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/operation/Dearmor.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.operation 6 | 7 | import java.io.IOException 8 | import java.io.InputStream 9 | import sop.Ready 10 | import sop.exception.SOPGPException.BadData 11 | import sop.util.UTF8Util 12 | 13 | interface Dearmor { 14 | 15 | /** 16 | * Dearmor armored OpenPGP data. 17 | * 18 | * @param data armored OpenPGP data 19 | * @return input stream of unarmored data 20 | * @throws BadData in case of non-OpenPGP data 21 | * @throws IOException in case of an IO error 22 | */ 23 | @Throws(BadData::class, IOException::class) fun data(data: InputStream): Ready 24 | 25 | /** 26 | * Dearmor armored OpenPGP data. 27 | * 28 | * @param data armored OpenPGP data 29 | * @return input stream of unarmored data 30 | * @throws BadData in case of non-OpenPGP data 31 | * @throws IOException in case of an IO error 32 | */ 33 | @Throws(BadData::class, IOException::class) 34 | fun data(data: ByteArray): Ready = data(data.inputStream()) 35 | 36 | /** 37 | * Dearmor amored OpenPGP data. 38 | * 39 | * @param data armored OpenPGP data 40 | * @return input stream of unarmored data 41 | * @throws BadData in case of non-OpenPGP data 42 | * @throws IOException in case of an IO error 43 | */ 44 | @Throws(BadData::class, IOException::class) 45 | fun data(data: String): Ready = data(data.toByteArray(UTF8Util.UTF8)) 46 | } 47 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/operation/DetachedSign.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.operation 6 | 7 | import java.io.IOException 8 | import java.io.InputStream 9 | import sop.ReadyWithResult 10 | import sop.SigningResult 11 | import sop.enums.SignAs 12 | import sop.exception.SOPGPException.* 13 | 14 | interface DetachedSign : AbstractSign { 15 | 16 | /** 17 | * Sets the signature mode. Note: This method has to be called before [key] is called. 18 | * 19 | * @param mode signature mode 20 | * @return builder instance 21 | * @throws UnsupportedOption if this option is not supported 22 | */ 23 | @Throws(UnsupportedOption::class) fun mode(mode: SignAs): DetachedSign 24 | 25 | /** 26 | * Signs data. 27 | * 28 | * @param data input stream containing data 29 | * @return ready 30 | * @throws IOException in case of an IO error 31 | * @throws KeyIsProtected if at least one signing key cannot be unlocked 32 | * @throws ExpectedText if text data was expected, but binary data was encountered 33 | */ 34 | @Throws(IOException::class, KeyIsProtected::class, ExpectedText::class) 35 | fun data(data: InputStream): ReadyWithResult 36 | 37 | /** 38 | * Signs data. 39 | * 40 | * @param data byte array containing data 41 | * @return ready 42 | * @throws IOException in case of an IO error 43 | * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be 44 | * unlocked 45 | * @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data 46 | * was encountered 47 | */ 48 | @Throws(IOException::class, KeyIsProtected::class, ExpectedText::class) 49 | fun data(data: ByteArray): ReadyWithResult = data(data.inputStream()) 50 | } 51 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.operation 6 | 7 | import java.io.IOException 8 | import java.io.InputStream 9 | import sop.exception.SOPGPException.BadData 10 | 11 | interface DetachedVerify : AbstractVerify, VerifySignatures { 12 | 13 | /** 14 | * Provides the detached signatures. 15 | * 16 | * @param signatures input stream containing encoded, detached signatures. 17 | * @return builder instance 18 | * @throws BadData if the input stream does not contain OpenPGP signatures 19 | * @throws IOException in case of an IO error 20 | */ 21 | @Throws(BadData::class, IOException::class) 22 | fun signatures(signatures: InputStream): VerifySignatures 23 | 24 | /** 25 | * Provides the detached signatures. 26 | * 27 | * @param signatures byte array containing encoded, detached signatures. 28 | * @return builder instance 29 | * @throws BadData if the byte array does not contain OpenPGP signatures 30 | * @throws IOException in case of an IO error 31 | */ 32 | @Throws(BadData::class, IOException::class) 33 | fun signatures(signatures: ByteArray): VerifySignatures = signatures(signatures.inputStream()) 34 | } 35 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/operation/ExtractCert.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.operation 6 | 7 | import java.io.IOException 8 | import java.io.InputStream 9 | import sop.Ready 10 | import sop.exception.SOPGPException.BadData 11 | 12 | interface ExtractCert { 13 | 14 | /** 15 | * Disable ASCII armor encoding. 16 | * 17 | * @return builder instance 18 | */ 19 | fun noArmor(): ExtractCert 20 | 21 | /** 22 | * Extract the cert(s) from the provided key(s). 23 | * 24 | * @param keyInputStream input stream containing the encoding of one or more OpenPGP keys 25 | * @return result containing the encoding of the keys certs 26 | * @throws IOException in case of an IO error 27 | * @throws BadData if the [InputStream] does not contain an OpenPGP key 28 | */ 29 | @Throws(IOException::class, BadData::class) fun key(keyInputStream: InputStream): Ready 30 | 31 | /** 32 | * Extract the cert(s) from the provided key(s). 33 | * 34 | * @param key byte array containing the encoding of one or more OpenPGP key 35 | * @return result containing the encoding of the keys certs 36 | * @throws IOException in case of an IO error 37 | * @throws BadData if the byte array does not contain an OpenPGP key 38 | */ 39 | @Throws(IOException::class, BadData::class) 40 | fun key(key: ByteArray): Ready = key(key.inputStream()) 41 | } 42 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/operation/GenerateKey.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.operation 6 | 7 | import java.io.IOException 8 | import sop.Profile 9 | import sop.Ready 10 | import sop.exception.SOPGPException.* 11 | import sop.util.UTF8Util 12 | 13 | interface GenerateKey { 14 | 15 | /** 16 | * Disable ASCII armor encoding. 17 | * 18 | * @return builder instance 19 | */ 20 | fun noArmor(): GenerateKey 21 | 22 | /** 23 | * Adds a user-id. 24 | * 25 | * @param userId user-id 26 | * @return builder instance 27 | */ 28 | fun userId(userId: String): GenerateKey 29 | 30 | /** 31 | * Set a password for the key. 32 | * 33 | * @param password password to protect the key 34 | * @return builder instance 35 | * @throws UnsupportedOption if key passwords are not supported 36 | * @throws PasswordNotHumanReadable if the password is not human-readable 37 | */ 38 | @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) 39 | fun withKeyPassword(password: String): GenerateKey 40 | 41 | /** 42 | * Set a password for the key. 43 | * 44 | * @param password password to protect the key 45 | * @return builder instance 46 | * @throws PasswordNotHumanReadable if the password is not human-readable 47 | * @throws UnsupportedOption if key passwords are not supported 48 | */ 49 | @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) 50 | fun withKeyPassword(password: ByteArray): GenerateKey = 51 | try { 52 | withKeyPassword(UTF8Util.decodeUTF8(password)) 53 | } catch (e: CharacterCodingException) { 54 | throw PasswordNotHumanReadable() 55 | } 56 | 57 | /** 58 | * Pass in a profile. 59 | * 60 | * @param profile profile 61 | * @return builder instance 62 | */ 63 | fun profile(profile: Profile): GenerateKey = profile(profile.name) 64 | 65 | /** 66 | * Pass in a profile identifier. 67 | * 68 | * @param profile profile identifier 69 | * @return builder instance 70 | */ 71 | fun profile(profile: String): GenerateKey 72 | 73 | /** 74 | * If this options is set, the generated key will not be capable of encryption / decryption. 75 | * 76 | * @return builder instance 77 | */ 78 | fun signingOnly(): GenerateKey 79 | 80 | /** 81 | * Generate the OpenPGP key and return it encoded as an [java.io.InputStream]. 82 | * 83 | * @return key 84 | * @throws MissingArg if no user-id was provided 85 | * @throws UnsupportedAsymmetricAlgo if the generated key uses an unsupported asymmetric 86 | * algorithm 87 | * @throws IOException in case of an IO error 88 | */ 89 | @Throws(MissingArg::class, UnsupportedAsymmetricAlgo::class, IOException::class) 90 | fun generate(): Ready 91 | } 92 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/operation/InlineDetach.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.operation 6 | 7 | import java.io.IOException 8 | import java.io.InputStream 9 | import sop.ReadyWithResult 10 | import sop.Signatures 11 | import sop.exception.SOPGPException.BadData 12 | 13 | interface InlineDetach { 14 | 15 | /** 16 | * Do not wrap the signatures in ASCII armor. 17 | * 18 | * @return builder 19 | */ 20 | fun noArmor(): InlineDetach 21 | 22 | /** 23 | * Detach the provided signed message from its signatures. 24 | * 25 | * @param messageInputStream input stream containing the signed message 26 | * @return result containing the detached message 27 | * @throws IOException in case of an IO error 28 | * @throws BadData if the input stream does not contain a signed message 29 | */ 30 | @Throws(IOException::class, BadData::class) 31 | fun message(messageInputStream: InputStream): ReadyWithResult 32 | 33 | /** 34 | * Detach the provided cleartext signed message from its signatures. 35 | * 36 | * @param message byte array containing the signed message 37 | * @return result containing the detached message 38 | * @throws IOException in case of an IO error 39 | * @throws BadData if the byte array does not contain a signed message 40 | */ 41 | @Throws(IOException::class, BadData::class) 42 | fun message(message: ByteArray): ReadyWithResult = message(message.inputStream()) 43 | } 44 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/operation/InlineSign.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.operation 6 | 7 | import java.io.IOException 8 | import java.io.InputStream 9 | import sop.Ready 10 | import sop.enums.InlineSignAs 11 | import sop.exception.SOPGPException.* 12 | 13 | interface InlineSign : AbstractSign { 14 | 15 | /** 16 | * Sets the signature mode. Note: This method has to be called before [.key] is called. 17 | * 18 | * @param mode signature mode 19 | * @return builder instance 20 | * @throws UnsupportedOption if this option is not supported 21 | */ 22 | @Throws(UnsupportedOption::class) fun mode(mode: InlineSignAs): InlineSign 23 | 24 | /** 25 | * Signs data. 26 | * 27 | * @param data input stream containing data 28 | * @return ready 29 | * @throws IOException in case of an IO error 30 | * @throws KeyIsProtected if at least one signing key cannot be unlocked 31 | * @throws ExpectedText if text data was expected, but binary data was encountered 32 | */ 33 | @Throws(IOException::class, KeyIsProtected::class, ExpectedText::class) 34 | fun data(data: InputStream): Ready 35 | 36 | /** 37 | * Signs data. 38 | * 39 | * @param data byte array containing data 40 | * @return ready 41 | * @throws IOException in case of an IO error 42 | * @throws KeyIsProtected if at least one signing key cannot be unlocked 43 | * @throws ExpectedText if text data was expected, but binary data was encountered 44 | */ 45 | @Throws(IOException::class, KeyIsProtected::class, ExpectedText::class) 46 | fun data(data: ByteArray): Ready = data(data.inputStream()) 47 | } 48 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/operation/InlineVerify.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.operation 6 | 7 | import java.io.IOException 8 | import java.io.InputStream 9 | import sop.ReadyWithResult 10 | import sop.Verification 11 | import sop.exception.SOPGPException.BadData 12 | import sop.exception.SOPGPException.NoSignature 13 | 14 | /** API for verification of inline-signed messages. */ 15 | interface InlineVerify : AbstractVerify { 16 | 17 | /** 18 | * Provide the inline-signed data. The result can be used to write the plaintext message out and 19 | * to get the verifications. 20 | * 21 | * @param data signed data 22 | * @return list of signature verifications 23 | * @throws IOException in case of an IO error 24 | * @throws NoSignature when no signature is found 25 | * @throws BadData when the data is invalid OpenPGP data 26 | */ 27 | @Throws(IOException::class, NoSignature::class, BadData::class) 28 | fun data(data: InputStream): ReadyWithResult> 29 | 30 | /** 31 | * Provide the inline-signed data. The result can be used to write the plaintext message out and 32 | * to get the verifications. 33 | * 34 | * @param data signed data 35 | * @return list of signature verifications 36 | * @throws IOException in case of an IO error 37 | * @throws NoSignature when no signature is found 38 | * @throws BadData when the data is invalid OpenPGP data 39 | */ 40 | @Throws(IOException::class, NoSignature::class, BadData::class) 41 | fun data(data: ByteArray): ReadyWithResult> = data(data.inputStream()) 42 | } 43 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/operation/ListProfiles.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.operation 6 | 7 | import sop.Profile 8 | 9 | /** Subcommand to list supported profiles of other subcommands. */ 10 | interface ListProfiles { 11 | 12 | /** 13 | * Provide the name of the subcommand for which profiles shall be listed. The returned list of 14 | * profiles MUST NOT contain more than 4 entries. 15 | * 16 | * @param command command name (e.g. `generate-key`) 17 | * @return list of profiles. 18 | */ 19 | fun subcommand(command: String): List 20 | 21 | /** 22 | * Return a list of [Profiles][Profile] supported by the [GenerateKey] implementation. 23 | * 24 | * @return profiles 25 | */ 26 | fun generateKey(): List = subcommand("generate-key") 27 | 28 | /** 29 | * Return a list of [Profiles][Profile] supported by the [Encrypt] implementation. 30 | * 31 | * @return profiles 32 | */ 33 | fun encrypt(): List = subcommand("encrypt") 34 | } 35 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/operation/RevokeKey.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.operation 6 | 7 | import java.io.InputStream 8 | import sop.Ready 9 | import sop.exception.SOPGPException.PasswordNotHumanReadable 10 | import sop.exception.SOPGPException.UnsupportedOption 11 | import sop.util.UTF8Util 12 | 13 | interface RevokeKey { 14 | 15 | /** 16 | * Disable ASCII armor encoding. 17 | * 18 | * @return builder instance 19 | */ 20 | fun noArmor(): RevokeKey 21 | 22 | /** 23 | * Provide the decryption password for the secret key. 24 | * 25 | * @param password password 26 | * @return builder instance 27 | * @throws UnsupportedOption if the implementation does not support key passwords 28 | * @throws PasswordNotHumanReadable if the password is not human-readable 29 | */ 30 | @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) 31 | fun withKeyPassword(password: String): RevokeKey = 32 | withKeyPassword(password.toByteArray(UTF8Util.UTF8)) 33 | 34 | /** 35 | * Provide the decryption password for the secret key. 36 | * 37 | * @param password password 38 | * @return builder instance 39 | * @throws UnsupportedOption if the implementation does not support key passwords 40 | * @throws PasswordNotHumanReadable if the password is not human-readable 41 | */ 42 | @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) 43 | fun withKeyPassword(password: ByteArray): RevokeKey 44 | 45 | fun keys(bytes: ByteArray): Ready = keys(bytes.inputStream()) 46 | 47 | fun keys(keys: InputStream): Ready 48 | } 49 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.operation 6 | 7 | import java.io.IOException 8 | import java.io.InputStream 9 | import sop.Verification 10 | import sop.exception.SOPGPException.BadData 11 | import sop.exception.SOPGPException.NoSignature 12 | 13 | interface VerifySignatures { 14 | 15 | /** 16 | * Provide the signed data (without signatures). 17 | * 18 | * @param data signed data 19 | * @return list of signature verifications 20 | * @throws IOException in case of an IO error 21 | * @throws NoSignature when no valid signature is found 22 | * @throws BadData when the data is invalid OpenPGP data 23 | */ 24 | @Throws(IOException::class, NoSignature::class, BadData::class) 25 | fun data(data: InputStream): List 26 | 27 | /** 28 | * Provide the signed data (without signatures). 29 | * 30 | * @param data signed data 31 | * @return list of signature verifications 32 | * @throws IOException in case of an IO error 33 | * @throws NoSignature when no valid signature is found 34 | * @throws BadData when the data is invalid OpenPGP data 35 | */ 36 | @Throws(IOException::class, NoSignature::class, BadData::class) 37 | fun data(data: ByteArray): List = data(data.inputStream()) 38 | } 39 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/util/HexUtil.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.util 6 | 7 | class HexUtil { 8 | 9 | companion object { 10 | /** 11 | * Encode a byte array to a hex string. 12 | * 13 | * @param bytes bytes 14 | * @return hex encoding 15 | * @see 16 | * [Convert Byte Arrays to Hex Strings in Kotlin](https://www.baeldung.com/kotlin/byte-arrays-to-hex-strings) 17 | */ 18 | @JvmStatic fun bytesToHex(bytes: ByteArray): String = bytes.toHex() 19 | 20 | /** 21 | * Decode a hex string into a byte array. 22 | * 23 | * @param s hex string 24 | * @return decoded byte array 25 | * @see 26 | * [Kotlin convert hex string to ByteArray](https://stackoverflow.com/a/66614516/11150851) 27 | */ 28 | @JvmStatic fun hexToBytes(s: String): ByteArray = s.decodeHex() 29 | } 30 | } 31 | 32 | fun String.decodeHex(): ByteArray { 33 | check(length % 2 == 0) { "Hex encoding must have even number of digits." } 34 | 35 | return chunked(2).map { it.toInt(16).toByte() }.toByteArray() 36 | } 37 | 38 | fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02X".format(eachByte) } 39 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/util/Optional.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.util 6 | 7 | /** 8 | * Backport of java.util.Optional for older Android versions. 9 | * 10 | * @param item type 11 | */ 12 | data class Optional(val item: T? = null) { 13 | 14 | val isPresent: Boolean = item != null 15 | val isEmpty: Boolean = item == null 16 | 17 | fun get() = item 18 | 19 | companion object { 20 | @JvmStatic fun of(item: T) = Optional(item!!) 21 | 22 | @JvmStatic fun ofNullable(item: T?) = Optional(item) 23 | 24 | @JvmStatic fun ofEmpty() = Optional(null as T?) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.util 6 | 7 | import java.io.ByteArrayOutputStream 8 | import java.io.IOException 9 | import java.io.OutputStream 10 | 11 | /** 12 | * [OutputStream] that buffers data being written into it, until its underlying output stream is 13 | * being replaced. At that point, first all the buffered data is being written to the underlying 14 | * stream, followed by any successive data that may get written to the [ProxyOutputStream]. This 15 | * class is useful if we need to provide an [OutputStream] at one point in time when the final 16 | * target output stream is not yet known. 17 | */ 18 | @Deprecated("Marked for removal.") 19 | // TODO: Remove in 11.X 20 | class ProxyOutputStream : OutputStream() { 21 | private val buffer = ByteArrayOutputStream() 22 | private var swapped: OutputStream? = null 23 | 24 | @Synchronized 25 | fun replaceOutputStream(underlying: OutputStream) { 26 | this.swapped = underlying 27 | swapped!!.write(buffer.toByteArray()) 28 | } 29 | 30 | @Synchronized 31 | @Throws(IOException::class) 32 | override fun write(b: ByteArray) { 33 | if (swapped == null) { 34 | buffer.write(b) 35 | } else { 36 | swapped!!.write(b) 37 | } 38 | } 39 | 40 | @Synchronized 41 | @Throws(IOException::class) 42 | override fun write(b: ByteArray, off: Int, len: Int) { 43 | if (swapped == null) { 44 | buffer.write(b, off, len) 45 | } else { 46 | swapped!!.write(b, off, len) 47 | } 48 | } 49 | 50 | @Synchronized 51 | @Throws(IOException::class) 52 | override fun flush() { 53 | buffer.flush() 54 | if (swapped != null) { 55 | swapped!!.flush() 56 | } 57 | } 58 | 59 | @Synchronized 60 | @Throws(IOException::class) 61 | override fun close() { 62 | buffer.close() 63 | if (swapped != null) { 64 | swapped!!.close() 65 | } 66 | } 67 | 68 | @Synchronized 69 | @Throws(IOException::class) 70 | override fun write(i: Int) { 71 | if (swapped == null) { 72 | buffer.write(i) 73 | } else { 74 | swapped!!.write(i) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/util/UTCUtil.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.util 6 | 7 | import java.text.ParseException 8 | import java.text.SimpleDateFormat 9 | import java.util.* 10 | 11 | class UTCUtil { 12 | 13 | companion object { 14 | 15 | @JvmField val UTC_FORMATTER = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") 16 | @JvmField 17 | val UTC_PARSERS = 18 | arrayOf( 19 | UTC_FORMATTER, 20 | SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"), 21 | SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"), 22 | SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")) 23 | .onEach { fmt -> fmt.timeZone = TimeZone.getTimeZone("UTC") } 24 | 25 | /** 26 | * Parse an ISO-8601 UTC timestamp from a string. 27 | * 28 | * @param dateString string 29 | * @return date 30 | * @throws ParseException if the date string is malformed and cannot be parsed 31 | */ 32 | @JvmStatic 33 | @Throws(ParseException::class) 34 | fun parseUTCDate(dateString: String): Date { 35 | var exception: ParseException? = null 36 | for (parser in UTC_PARSERS) { 37 | try { 38 | return parser.parse(dateString) 39 | } catch (e: ParseException) { 40 | // Store first exception (that of UTC_FORMATTER) to throw if we fail to parse 41 | // the date 42 | if (exception == null) { 43 | exception = e 44 | } 45 | // Try next parser 46 | } 47 | } 48 | throw exception!! 49 | } 50 | 51 | /** 52 | * Format a date as ISO-8601 UTC timestamp. 53 | * 54 | * @param date date 55 | * @return timestamp string 56 | */ 57 | @JvmStatic 58 | fun formatUTCDate(date: Date): String { 59 | return UTC_FORMATTER.format(date) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /sop-java/src/main/kotlin/sop/util/UTF8Util.kt: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.util 6 | 7 | import java.nio.ByteBuffer 8 | import java.nio.charset.Charset 9 | import java.nio.charset.CodingErrorAction 10 | 11 | class UTF8Util { 12 | companion object { 13 | @JvmField val UTF8: Charset = Charset.forName("UTF8") 14 | 15 | @JvmStatic 16 | private val UTF8Decoder = 17 | UTF8.newDecoder() 18 | .onUnmappableCharacter(CodingErrorAction.REPORT) 19 | .onMalformedInput(CodingErrorAction.REPORT) 20 | 21 | /** 22 | * Detect non-valid UTF8 data. 23 | * 24 | * @param data utf-8 encoded bytes 25 | * @return decoded string 26 | * @throws CharacterCodingException if the input data does not resemble UTF8 27 | * @see [ante on StackOverflow](https://stackoverflow.com/a/1471193) 28 | */ 29 | @JvmStatic 30 | @Throws(CharacterCodingException::class) 31 | fun decodeUTF8(data: ByteArray): String { 32 | val byteBuffer = ByteBuffer.wrap(data) 33 | val charBuffer = UTF8Decoder.decode(byteBuffer) 34 | return charBuffer.toString() 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sop-java/src/main/resources/sop-java-version.properties: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 Paul Schaub 2 | # 3 | # SPDX-License-Identifier: Apache-2.0 4 | sop-java-version=@project.version@ -------------------------------------------------------------------------------- /sop-java/src/test/java/sop/ByteArrayAndResultTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import sop.util.UTCUtil; 9 | 10 | import java.nio.charset.StandardCharsets; 11 | import java.text.ParseException; 12 | import java.util.Collections; 13 | import java.util.List; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 16 | import static org.junit.jupiter.api.Assertions.assertEquals; 17 | 18 | public class ByteArrayAndResultTest { 19 | 20 | @Test 21 | public void testCreationAndGetters() throws ParseException { 22 | byte[] bytes = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); 23 | List result = Collections.singletonList( 24 | new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"), 25 | "C90E6D36200A1B922A1509E77618196529AE5FF8", 26 | "C4BC2DDB38CCE96485EBE9C2F20691179038E5C6") 27 | ); 28 | ByteArrayAndResult> bytesAndResult = new ByteArrayAndResult<>(bytes, result); 29 | 30 | assertArrayEquals(bytes, bytesAndResult.getBytes()); 31 | assertEquals(result, bytesAndResult.getResult()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sop-java/src/test/java/sop/MicAlgTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertNotNull; 14 | import static org.junit.jupiter.api.Assertions.assertThrows; 15 | import static org.junit.jupiter.api.Assertions.assertTrue; 16 | 17 | public class MicAlgTest { 18 | 19 | @Test 20 | public void constructorNullArgThrows() { 21 | assertThrows(NullPointerException.class, () -> new MicAlg(null)); 22 | } 23 | 24 | @Test 25 | public void emptyMicAlgIsEmptyString() { 26 | MicAlg empty = MicAlg.empty(); 27 | assertNotNull(empty.getMicAlg()); 28 | assertTrue(empty.getMicAlg().isEmpty()); 29 | } 30 | 31 | @Test 32 | public void fromInvalidAlgorithmIdThrows() { 33 | assertThrows(IllegalArgumentException.class, () -> MicAlg.fromHashAlgorithmId(-1)); 34 | } 35 | 36 | @Test 37 | public void fromHashAlgorithmIdsKnownAlgsMatch() { 38 | Map knownAlgorithmMicalgs = new HashMap<>(); 39 | knownAlgorithmMicalgs.put(1, "pgp-md5"); 40 | knownAlgorithmMicalgs.put(2, "pgp-sha1"); 41 | knownAlgorithmMicalgs.put(3, "pgp-ripemd160"); 42 | knownAlgorithmMicalgs.put(8, "pgp-sha256"); 43 | knownAlgorithmMicalgs.put(9, "pgp-sha384"); 44 | knownAlgorithmMicalgs.put(10, "pgp-sha512"); 45 | knownAlgorithmMicalgs.put(11, "pgp-sha224"); 46 | 47 | for (Integer id : knownAlgorithmMicalgs.keySet()) { 48 | MicAlg micAlg = MicAlg.fromHashAlgorithmId(id); 49 | assertEquals(knownAlgorithmMicalgs.get(id), micAlg.getMicAlg()); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sop-java/src/test/java/sop/ReadyTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.io.IOException; 10 | import java.io.OutputStream; 11 | import java.nio.charset.StandardCharsets; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 14 | 15 | public class ReadyTest { 16 | 17 | @Test 18 | public void readyTest() throws IOException { 19 | byte[] data = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); 20 | Ready ready = new Ready() { 21 | @Override 22 | public void writeTo(OutputStream outputStream) throws IOException { 23 | outputStream.write(data); 24 | } 25 | }; 26 | 27 | assertArrayEquals(data, ready.getBytes()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sop-java/src/test/java/sop/ReadyWithResultTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import sop.exception.SOPGPException; 9 | import sop.util.UTCUtil; 10 | 11 | import java.io.IOException; 12 | import java.io.OutputStream; 13 | import java.nio.charset.StandardCharsets; 14 | import java.text.ParseException; 15 | import java.util.Collections; 16 | import java.util.List; 17 | 18 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 19 | import static org.junit.jupiter.api.Assertions.assertEquals; 20 | 21 | public class ReadyWithResultTest { 22 | 23 | @Test 24 | public void testReadyWithResult() throws SOPGPException.NoSignature, IOException, ParseException { 25 | byte[] data = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); 26 | List result = Collections.singletonList( 27 | new Verification(UTCUtil.parseUTCDate("2019-10-24T23:48:29Z"), 28 | "C90E6D36200A1B922A1509E77618196529AE5FF8", 29 | "C4BC2DDB38CCE96485EBE9C2F20691179038E5C6") 30 | ); 31 | ReadyWithResult> readyWithResult = new ReadyWithResult>() { 32 | @Override 33 | public List writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { 34 | outputStream.write(data); 35 | return result; 36 | } 37 | }; 38 | 39 | ByteArrayAndResult> bytesAndResult = readyWithResult.toByteArrayAndResult(); 40 | assertArrayEquals(data, bytesAndResult.getBytes()); 41 | assertEquals(result, bytesAndResult.getResult()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sop-java/src/test/java/sop/SessionKeyTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import sop.util.HexUtil; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 12 | import static org.junit.jupiter.api.Assertions.assertThrows; 13 | 14 | public class SessionKeyTest { 15 | 16 | @Test 17 | public void fromStringTest() { 18 | String string = "9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"; 19 | SessionKey sessionKey = SessionKey.fromString(string); 20 | assertEquals(string, sessionKey.toString()); 21 | } 22 | 23 | @Test 24 | public void fromLowerStringTest() { 25 | String string = "9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"; 26 | String lowercaseWithTrailingNewLine = "9:fca4beaf687f48059cacc14fb019125cd57392bab7037c707835925cbf9f7bcd\n"; 27 | SessionKey sessionKey = SessionKey.fromString(lowercaseWithTrailingNewLine); 28 | assertEquals(string, sessionKey.toString()); 29 | } 30 | 31 | @Test 32 | public void toStringTest() { 33 | SessionKey sessionKey = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); 34 | assertEquals("9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD", sessionKey.toString()); 35 | } 36 | 37 | @Test 38 | public void equalsTest() { 39 | SessionKey s1 = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); 40 | SessionKey s2 = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); 41 | SessionKey s3 = new SessionKey((byte) 4, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); 42 | SessionKey s4 = new SessionKey((byte) 9, HexUtil.hexToBytes("19125CD57392BAB7037C7078359FCA4BEAF687F4025CBF9F7BCD8059CACC14FB")); 43 | SessionKey s5 = new SessionKey((byte) 4, HexUtil.hexToBytes("19125CD57392BAB7037C7078359FCA4BEAF687F4025CBF9F7BCD8059CACC14FB")); 44 | 45 | assertEquals(s1, s1); 46 | assertEquals(s1, s2); 47 | assertEquals(s1.hashCode(), s2.hashCode()); 48 | assertNotEquals(s1, s3); 49 | assertNotEquals(s1.hashCode(), s3.hashCode()); 50 | assertNotEquals(s1, s4); 51 | assertNotEquals(s1.hashCode(), s4.hashCode()); 52 | assertNotEquals(s4, s5); 53 | assertNotEquals(s4.hashCode(), s5.hashCode()); 54 | assertNotEquals(s1, null); 55 | assertNotEquals(s1, "FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"); 56 | } 57 | 58 | @Test 59 | public void fromString_missingAlgorithmIdThrows() { 60 | String missingAlgorithId = "FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"; 61 | assertThrows(IllegalArgumentException.class, () -> SessionKey.fromString(missingAlgorithId)); 62 | } 63 | 64 | @Test 65 | public void fromString_wrongDivider() { 66 | String semicolonDivider = "9;FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"; 67 | assertThrows(IllegalArgumentException.class, () -> SessionKey.fromString(semicolonDivider)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /sop-java/src/test/java/sop/SigningResultTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | 11 | public class SigningResultTest { 12 | 13 | @Test 14 | public void basicBuilderTest() { 15 | SigningResult result = SigningResult.builder() 16 | .setMicAlg(MicAlg.fromHashAlgorithmId(10)) 17 | .build(); 18 | 19 | assertEquals("pgp-sha512", result.getMicAlg().getMicAlg()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sop-java/src/test/java/sop/util/HexUtilTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.util; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | import java.nio.charset.Charset; 11 | 12 | import org.junit.jupiter.api.Test; 13 | 14 | /** 15 | * Test using some test vectors from RFC4648. 16 | * 17 | * @see RFC-4648 §10: Test Vectors 18 | */ 19 | public class HexUtilTest { 20 | 21 | @SuppressWarnings("CharsetObjectCanBeUsed") 22 | private static final Charset ASCII = Charset.forName("US-ASCII"); 23 | 24 | @Test 25 | public void emptyHexEncodeTest() { 26 | assertHexEquals("", ""); 27 | } 28 | 29 | @Test 30 | public void encodeF() { 31 | assertHexEquals("66", "f"); 32 | } 33 | 34 | @Test 35 | public void encodeFo() { 36 | assertHexEquals("666F", "fo"); 37 | } 38 | 39 | @Test 40 | public void encodeFoo() { 41 | assertHexEquals("666F6F", "foo"); 42 | } 43 | 44 | @Test 45 | public void encodeFoob() { 46 | assertHexEquals("666F6F62", "foob"); 47 | } 48 | 49 | @Test 50 | public void encodeFooba() { 51 | assertHexEquals("666F6F6261", "fooba"); 52 | } 53 | 54 | @Test 55 | public void encodeFoobar() { 56 | assertHexEquals("666F6F626172", "foobar"); 57 | } 58 | 59 | private void assertHexEquals(String hex, String ascii) { 60 | assertEquals(hex, HexUtil.bytesToHex(ascii.getBytes(ASCII))); 61 | assertArrayEquals(ascii.getBytes(ASCII), HexUtil.hexToBytes(hex)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /sop-java/src/test/java/sop/util/OptionalTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.util; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertFalse; 9 | import static org.junit.jupiter.api.Assertions.assertNull; 10 | import static org.junit.jupiter.api.Assertions.assertThrows; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | import org.junit.jupiter.api.Test; 14 | 15 | public class OptionalTest { 16 | 17 | @Test 18 | public void testEmpty() { 19 | Optional optional = new Optional<>(); 20 | assertEmpty(optional); 21 | } 22 | 23 | @Test 24 | public void testArg() { 25 | String string = "foo"; 26 | Optional optional = new Optional<>(string); 27 | assertFalse(optional.isEmpty()); 28 | assertTrue(optional.isPresent()); 29 | assertEquals(string, optional.get()); 30 | } 31 | 32 | @Test 33 | public void testOfEmpty() { 34 | Optional optional = Optional.ofEmpty(); 35 | assertEmpty(optional); 36 | } 37 | 38 | @Test 39 | public void testNullArg() { 40 | Optional optional = new Optional<>(null); 41 | assertEmpty(optional); 42 | } 43 | 44 | @Test 45 | public void testOfWithNullArgThrows() { 46 | assertThrows(NullPointerException.class, () -> Optional.of(null)); 47 | } 48 | 49 | @Test 50 | public void testOf() { 51 | String string = "Hello, World!"; 52 | Optional optional = Optional.of(string); 53 | assertFalse(optional.isEmpty()); 54 | assertTrue(optional.isPresent()); 55 | assertEquals(string, optional.get()); 56 | } 57 | 58 | @Test 59 | public void testOfNullableWithNull() { 60 | Optional optional = Optional.ofNullable(null); 61 | assertEmpty(optional); 62 | } 63 | 64 | @Test 65 | public void testOfNullableWithArg() { 66 | Optional optional = Optional.ofNullable("bar"); 67 | assertEquals("bar", optional.get()); 68 | assertFalse(optional.isEmpty()); 69 | assertTrue(optional.isPresent()); 70 | } 71 | 72 | private void assertEmpty(Optional optional) { 73 | assertTrue(optional.isEmpty()); 74 | assertFalse(optional.isPresent()); 75 | 76 | assertNull(optional.get()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.util; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertThrows; 9 | 10 | import java.io.ByteArrayOutputStream; 11 | import java.io.IOException; 12 | import java.nio.charset.StandardCharsets; 13 | 14 | import org.junit.jupiter.api.Test; 15 | 16 | public class ProxyOutputStreamTest { 17 | 18 | @Test 19 | public void replaceOutputStreamThrowsNPEForNull() { 20 | ProxyOutputStream proxy = new ProxyOutputStream(); 21 | assertThrows(NullPointerException.class, () -> proxy.replaceOutputStream(null)); 22 | } 23 | 24 | @Test 25 | public void testSwappingStreamPreservesWrittenBytes() throws IOException { 26 | byte[] firstSection = "Foo\nBar\n".getBytes(StandardCharsets.UTF_8); 27 | byte[] secondSection = "Baz\n".getBytes(StandardCharsets.UTF_8); 28 | 29 | ProxyOutputStream proxy = new ProxyOutputStream(); 30 | proxy.write(firstSection); 31 | 32 | ByteArrayOutputStream swappedStream = new ByteArrayOutputStream(); 33 | proxy.replaceOutputStream(swappedStream); 34 | 35 | proxy.write(secondSection); 36 | proxy.close(); 37 | 38 | assertEquals("Foo\nBar\nBaz\n", swappedStream.toString()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sop-java/src/test/java/sop/util/UTCUtilTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.util; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.text.ParseException; 10 | import java.util.Date; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertThrows; 14 | 15 | /** 16 | * Test parsing some date examples from the stateless OpenPGP CLI spec. 17 | * 18 | * @see OpenPGP Stateless CLI §4.1. Date 19 | */ 20 | public class UTCUtilTest { 21 | 22 | @Test 23 | public void parseExample1() throws ParseException { 24 | String timestamp = "2019-10-29T12:11:04+00:00"; 25 | Date date = UTCUtil.parseUTCDate(timestamp); 26 | assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date)); 27 | } 28 | 29 | @Test 30 | public void parseExample2() throws ParseException { 31 | String timestamp = "2019-10-24T23:48:29Z"; 32 | Date date = UTCUtil.parseUTCDate(timestamp); 33 | assertEquals("2019-10-24T23:48:29Z", UTCUtil.formatUTCDate(date)); 34 | } 35 | 36 | @Test 37 | public void parseExample3() throws ParseException { 38 | String timestamp = "20191029T121104Z"; 39 | Date date = UTCUtil.parseUTCDate(timestamp); 40 | assertEquals("2019-10-29T12:11:04Z", UTCUtil.formatUTCDate(date)); 41 | } 42 | 43 | @Test 44 | public void invalidDateThrows() { 45 | String invalidTimestamp = "foobar"; 46 | assertThrows(ParseException.class, () -> UTCUtil.parseUTCDate(invalidTimestamp)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sop-java/src/test/java/sop/util/UTF8UtilTest.java: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | package sop.util; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.nio.charset.CharacterCodingException; 10 | import java.nio.charset.StandardCharsets; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertThrows; 14 | 15 | public class UTF8UtilTest { 16 | 17 | @Test 18 | public void testValidUtf8Decoding() throws CharacterCodingException { 19 | String utf8String = "Hello, World\n"; 20 | String decoded = UTF8Util.decodeUTF8(utf8String.getBytes(StandardCharsets.UTF_8)); 21 | 22 | assertEquals(utf8String, decoded); 23 | } 24 | 25 | /** 26 | * Test detection of non-uft8 data. 27 | * @see 28 | * Markus Kuhn's UTF8 decoder capability and stress test file 29 | */ 30 | @Test 31 | public void testInvalidUtf8StringThrows() { 32 | assertThrows(CharacterCodingException.class, 33 | () -> UTF8Util.decodeUTF8(new byte[] {(byte) 0xa0, (byte) 0xa1})); 34 | assertThrows(CharacterCodingException.class, 35 | () -> UTF8Util.decodeUTF8(new byte[] {(byte) 0xc0, (byte) 0xaf})); 36 | assertThrows(CharacterCodingException.class, 37 | () -> UTF8Util.decodeUTF8(new byte[] {(byte) 0x80, (byte) 0xbf})); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /version.gradle: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 Paul Schaub 2 | // 3 | // SPDX-License-Identifier: CC0-1.0 4 | 5 | allprojects { 6 | ext { 7 | shortVersion = '10.1.2' 8 | isSnapshot = true 9 | minAndroidSdk = 10 10 | javaSourceCompatibility = 11 11 | gsonVersion = '2.10.1' 12 | jsrVersion = '3.0.2' 13 | junitVersion = '5.8.2' 14 | junitSysExitVersion = '1.1.2' 15 | logbackVersion = '1.2.13' // 1.4+ cause CLI spam 16 | mockitoVersion = '4.5.1' 17 | picocliVersion = '4.6.3' 18 | slf4jVersion = '1.7.36' 19 | } 20 | } 21 | --------------------------------------------------------------------------------