Compare commits

...

40 commits

Author SHA1 Message Date
69dd846667
Fix documentation of merge-certs command 2024-09-19 20:49:00 +02:00
f98869d71b
Bump version 2024-09-19 20:38:34 +02:00
b2099dc508
Document endOfOptionsDelimiter 2024-09-19 20:38:34 +02:00
c7a4087763
Fix nullability of sop commands 2024-09-19 20:38:34 +02:00
eb712e6853
Add first test for new commands 2024-09-19 20:38:34 +02:00
ce19207a92
Show endOfOptions delimiter in help 2024-09-19 20:38:34 +02:00
8c19f2f31d
Implement external variants of new subcommands 2024-09-19 20:38:33 +02:00
2ec7088c12
Checkstyle 2024-09-19 20:38:33 +02:00
0ec2961cbe
Implement validate-userid command 2024-09-19 20:38:33 +02:00
1945411712
Update msg files with input/output information 2024-09-19 20:38:33 +02:00
5e1915c3ce
Checkstyle and exception handling improvements 2024-09-19 20:38:33 +02:00
3104085fe7
Implement certify-userid command 2024-09-19 20:38:32 +02:00
a607013cfb
Add support for rendering help info for input and output 2024-09-19 20:38:32 +02:00
ddf4ba19f9
Add implementation of merge-certs command 2024-09-19 20:38:32 +02:00
a2315f9847
Add implementation of update-key command 2024-09-19 20:38:32 +02:00
a07446e29a
Add new exceptions 2024-09-19 20:38:32 +02:00
5f18edd2e7
EncryptExternal: Fix parameter passing for --sign-with option 2024-09-19 20:38:18 +02:00
8134e3cd12
Update changelog 2024-09-19 20:16:11 +02:00
8843d217e4
revoke-key command: Allow for multiple '--with-key-password' options 2024-09-19 20:13:12 +02:00
61f3bf14c5
Fix NPE in DecryptExternal when reading lines 2024-09-19 20:00:30 +02:00
ed9fb31549
Fix woodpecker warnings 2024-09-19 19:38:59 +02:00
8422a23396
Update changelog 2024-09-19 19:25:38 +02:00
ce21e07fc2
Properly match MissingArg exception code 2024-09-19 19:23:07 +02:00
ec90d2e951
Add translations for new hardware exception error messages 2024-07-09 14:39:03 +02:00
d5c011ea4a
Add new SOPGPException types related to hardware modules 2024-07-09 14:29:22 +02:00
1d80ff1d8d
Update changelog 2024-03-30 19:02:58 +01:00
9356447226
Remove label() option from armor() operation 2024-03-30 19:00:09 +01:00
a13f1e2a0d
Mark ProxyOutputStream as deprecated 2024-03-27 21:57:04 +01:00
e39cc7f0ac
Remove deprecated junit5-system-exit
Replaced with custom test DSL that avoids System.exit
2024-03-27 21:50:01 +01:00
cbbdd09472
SOP-Java 10.0.1-SNAPSHOT 2024-03-21 14:06:42 +01:00
da6cba1d55
SOP-Java 10.0.0 2024-03-21 14:01:48 +01:00
4b2875d572
Update changelog 2024-03-21 13:54:56 +01:00
30f7ca90cd
Bump logback-core and logback-classic from 1.2.11 to 1.2.13 2024-03-21 13:54:56 +01:00
bfa97aede8
Add description of external-sop module 2024-03-21 13:54:56 +01:00
bdbc9593c8
Update spec revision and badge link 2024-03-21 13:54:56 +01:00
3643aff082
Bump version to 10.0.0 2024-03-21 13:54:55 +01:00
ed9b2f5fef
Move signature verification operations to sopv interface subset 2024-03-21 13:54:55 +01:00
cd208c8942
Add test ckecking that BadData is thrown if KEYS is passed for CERTS 2024-03-21 13:54:55 +01:00
7325cad696
Replace assumeTrue(false) with explicit TestAbortedException 2024-03-21 13:54:55 +01:00
7a825c7607
Fix javadoc reference 2024-03-21 13:54:55 +01:00
93 changed files with 2076 additions and 367 deletions

View file

@ -1,5 +1,7 @@
steps: steps:
run: run:
when:
event: push
image: gradle:7.6-jdk11-jammy image: gradle:7.6-jdk11-jammy
commands: commands:
# Install Sequoia-SOP # Install Sequoia-SOP

View file

@ -2,6 +2,8 @@
# See https://reuse.software/ # See https://reuse.software/
steps: steps:
reuse: reuse:
when:
event: push
image: fsfe/reuse:latest image: fsfe/reuse:latest
commands: commands:
- reuse lint - reuse lint

View file

@ -6,6 +6,17 @@ SPDX-License-Identifier: Apache-2.0
# Changelog # Changelog
## 10.0.1-SNAPSHOT
- Remove `label()` option from `Armor` operation
- Fix exit code for 'Missing required option/parameter' error
- Fix `revoke-key`: Allow for multiple invocations of `--with-key-password` option
## 10.0.0
- Update implementation to [SOP Specification revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html).
- Throw `BadData` when passing KEYS where CERTS are expected
- Introduce `sopv` interface subset with revision `1.0`
- Add `sop version --sopv`
## 8.0.1 ## 8.0.1
- `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty. - `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty.

View file

@ -7,7 +7,7 @@ SPDX-License-Identifier: Apache-2.0
# SOP for Java # SOP for Java
[![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java) [![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java)
[![Spec Revision: 8](https://img.shields.io/badge/Spec%20Revision-8-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/08/) [![Spec Revision: 10](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/10/)
[![Coverage Status](https://coveralls.io/repos/github/pgpainless/sop-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/sop-java?branch=main) [![Coverage Status](https://coveralls.io/repos/github/pgpainless/sop-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/sop-java?branch=main)
[![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/sop-java)](https://api.reuse.software/info/github.com/pgpainless/sop-java) [![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/sop-java)](https://api.reuse.software/info/github.com/pgpainless/sop-java)
@ -25,6 +25,8 @@ The repository contains the following modules:
* [sop-java](/sop-java) defines a set of Java interfaces describing the Stateless OpenPGP Protocol. * [sop-java](/sop-java) defines a set of Java interfaces describing the Stateless OpenPGP Protocol.
* [sop-java-picocli](/sop-java-picocli) contains a wrapper application that transforms the `sop-java` API into a command line application * [sop-java-picocli](/sop-java-picocli) contains a wrapper application that transforms the `sop-java` API into a command line application
compatible with the SOP-CLI specification. compatible with the SOP-CLI specification.
* [external-sop](/external-sop) contains an API implementation that can be used to forward API calls to a SOP executable,
allowing to delegate the implementation logic to an arbitrary SOP CLI implementation.
## Known Implementations ## Known Implementations
(Please expand!) (Please expand!)

View file

@ -16,7 +16,7 @@ import sop.external.operation.*
import sop.operation.* import sop.operation.*
/** /**
* Implementation of the {@link SOP} API using an external SOP binary. * Implementation of the [SOP] API using an external SOP binary.
* *
* Instantiate an [ExternalSOP] object for the given binary and the given [TempDirProvider] using * Instantiate an [ExternalSOP] object for the given binary and the given [TempDirProvider] using
* empty environment variables. * empty environment variables.
@ -69,6 +69,14 @@ class ExternalSOP(
override fun changeKeyPassword(): ChangeKeyPassword = override fun changeKeyPassword(): ChangeKeyPassword =
ChangeKeyPasswordExternal(binaryName, properties) ChangeKeyPasswordExternal(binaryName, properties)
override fun updateKey(): UpdateKey = UpdateKeyExternal(binaryName, properties)
override fun mergeCerts(): MergeCerts = MergeCertsExternal(binaryName, properties)
override fun certifyUserId(): CertifyUserId = CertifyUserIdExternal(binaryName, properties)
override fun validateUserId(): ValidateUserId = ValidateUserIdExternal(binaryName, properties)
/** /**
* This interface can be used to provide a directory in which external SOP binaries can * This interface can be used to provide a directory in which external SOP binaries can
* temporarily store additional results of OpenPGP operations such that the binding classes can * temporarily store additional results of OpenPGP operations such that the binding classes can
@ -169,6 +177,18 @@ class ExternalSOP(
UnsupportedProfile.EXIT_CODE -> UnsupportedProfile.EXIT_CODE ->
throw UnsupportedProfile( throw UnsupportedProfile(
"External SOP backend reported error UnsupportedProfile ($exitCode):\n$errorMessage") "External SOP backend reported error UnsupportedProfile ($exitCode):\n$errorMessage")
NoHardwareKeyFound.EXIT_CODE ->
throw NoHardwareKeyFound(
"External SOP backend reported error NoHardwareKeyFound ($exitCode):\n$errorMessage")
HardwareKeyFailure.EXIT_CODE ->
throw HardwareKeyFailure(
"External SOP backend reported error HardwareKeyFalure ($exitCode):\n$errorMessage")
PrimaryKeyBad.EXIT_CODE ->
throw PrimaryKeyBad(
"External SOP backend reported error PrimaryKeyBad ($exitCode):\n$errorMessage")
CertUserIdNoMatch.EXIT_CODE ->
throw CertUserIdNoMatch(
"External SOP backend reported error CertUserIdNoMatch ($exitCode):\n$errorMessage")
// Did you forget to add a case for a new exception type? // Did you forget to add a case for a new exception type?
else -> else ->

View file

@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.external
import java.nio.file.Files
import java.util.*
import sop.SOPV
import sop.external.ExternalSOP.TempDirProvider
import sop.external.operation.DetachedVerifyExternal
import sop.external.operation.InlineVerifyExternal
import sop.external.operation.VersionExternal
import sop.operation.DetachedVerify
import sop.operation.InlineVerify
import sop.operation.Version
/**
* Implementation of the [SOPV] API subset using an external sopv/sop binary.
*
* Instantiate an [ExternalSOPV] object for the given binary and the given [TempDirProvider] using
* empty environment variables.
*
* @param binaryName name / path of the sopv binary
* @param tempDirProvider custom tempDirProvider
*/
class ExternalSOPV(
private val binaryName: String,
private val properties: Properties = Properties(),
private val tempDirProvider: TempDirProvider = defaultTempDirProvider()
) : SOPV {
override fun version(): Version = VersionExternal(binaryName, properties)
override fun detachedVerify(): DetachedVerify = DetachedVerifyExternal(binaryName, properties)
override fun inlineVerify(): InlineVerify =
InlineVerifyExternal(binaryName, properties, tempDirProvider)
companion object {
/**
* Default implementation of the [TempDirProvider] which stores temporary files in the
* systems temp dir ([Files.createTempDirectory]).
*
* @return default implementation
*/
@JvmStatic
fun defaultTempDirProvider(): TempDirProvider {
return TempDirProvider { Files.createTempDirectory("ext-sopv").toFile() }
}
}
}

View file

@ -7,7 +7,6 @@ package sop.external.operation
import java.io.InputStream import java.io.InputStream
import java.util.Properties import java.util.Properties
import sop.Ready import sop.Ready
import sop.enums.ArmorLabel
import sop.exception.SOPGPException import sop.exception.SOPGPException
import sop.external.ExternalSOP import sop.external.ExternalSOP
import sop.operation.Armor import sop.operation.Armor
@ -18,8 +17,6 @@ class ArmorExternal(binary: String, environment: Properties) : Armor {
private val commandList: MutableList<String> = mutableListOf(binary, "armor") private val commandList: MutableList<String> = mutableListOf(binary, "armor")
private val envList: List<String> = ExternalSOP.propertiesToEnv(environment) private val envList: List<String> = ExternalSOP.propertiesToEnv(environment)
override fun label(label: ArmorLabel): Armor = apply { commandList.add("--label=$label") }
@Throws(SOPGPException.BadData::class) @Throws(SOPGPException.BadData::class)
override fun data(data: InputStream): Ready = override fun data(data: InputStream): Ready =
ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data)

View file

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

View file

@ -108,8 +108,8 @@ class DecryptExternal(
finish(process) finish(process)
val sessionKeyOutIn = FileInputStream(sessionKeyOut) val sessionKeyOutIn = FileInputStream(sessionKeyOut)
var line = readString(sessionKeyOutIn) var line: String? = readString(sessionKeyOutIn)
val sessionKey = SessionKey.fromString(line.trim { it <= ' ' }) val sessionKey = line?.let { l -> SessionKey.fromString(l.trim { it <= ' ' }) }
sessionKeyOutIn.close() sessionKeyOutIn.close()
sessionKeyOut.delete() sessionKeyOut.delete()
@ -118,7 +118,7 @@ class DecryptExternal(
val verifyOutIn = FileInputStream(verifyOut) val verifyOutIn = FileInputStream(verifyOut)
val reader = BufferedReader(InputStreamReader(verifyOutIn)) val reader = BufferedReader(InputStreamReader(verifyOutIn))
while (reader.readLine().also { line = it } != null) { while (reader.readLine().also { line = it } != null) {
verifications.add(Verification.fromString(line.trim())) line?.let { verifications.add(Verification.fromString(it.trim())) }
} }
reader.close() reader.close()
} }

View file

@ -36,7 +36,7 @@ class EncryptExternal(
override fun mode(mode: EncryptAs): Encrypt = apply { commandList.add("--as=$mode") } override fun mode(mode: EncryptAs): Encrypt = apply { commandList.add("--as=$mode") }
override fun signWith(key: InputStream): Encrypt = apply { override fun signWith(key: InputStream): Encrypt = apply {
commandList.add("--sign-with@ENV:SIGN_WITH_$argCounter") commandList.add("--sign-with=@ENV:SIGN_WITH_$argCounter")
envList.add("SIGN_WITH_$argCounter=${ExternalSOP.readString(key)}") envList.add("SIGN_WITH_$argCounter=${ExternalSOP.readString(key)}")
argCounter += 1 argCounter += 1
} }

View file

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

View file

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

View file

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

View file

@ -68,6 +68,10 @@ class VersionExternal(binary: String, environment: Properties) : Version {
return null return null
} }
override fun getSopVVersion(): String {
return executeForLines(commandList.plus("--sopv"))
}
override fun getSopSpecVersion(): String { override fun getSopSpecVersion(): String {
return executeForLines(commandList.plus("--sop-spec")) return executeForLines(commandList.plus("--sop-spec"))
} }

View file

@ -12,15 +12,12 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
// Testing Exit Codes in JUnit
// https://todd.ginsberg.com/post/testing-system-exit/
testImplementation "com.ginsberg:junit5-system-exit:$junitSysExitVersion"
// Mocking Components // Mocking Components
testImplementation "org.mockito:mockito-core:$mockitoVersion" testImplementation "org.mockito:mockito-core:$mockitoVersion"
// SOP // SOP
implementation(project(":sop-java")) implementation(project(":sop-java"))
testImplementation(testFixtures(project(":sop-java")))
// CLI // CLI
implementation "info.picocli:picocli:$picocliVersion" implementation "info.picocli:picocli:$picocliVersion"

View file

@ -21,6 +21,8 @@ class SOPExceptionExitCodeMapper : IExitCodeExceptionMapper {
// Unmatched subcommand // Unmatched subcommand
SOPGPException.UnsupportedSubcommand.EXIT_CODE SOPGPException.UnsupportedSubcommand.EXIT_CODE
} }
} else if (exception is MissingParameterException) {
SOPGPException.MissingArg.EXIT_CODE
} else if (exception is ParameterException) { } else if (exception is ParameterException) {
// Invalid option (e.g. `--as invalid`) // Invalid option (e.g. `--as invalid`)
SOPGPException.UnsupportedOption.EXIT_CODE SOPGPException.UnsupportedOption.EXIT_CODE

View file

@ -27,6 +27,10 @@ import sop.exception.SOPGPException
ChangeKeyPasswordCmd::class, ChangeKeyPasswordCmd::class,
RevokeKeyCmd::class, RevokeKeyCmd::class,
ExtractCertCmd::class, ExtractCertCmd::class,
UpdateKeyCmd::class,
MergeCertsCmd::class,
CertifyUserIdCmd::class,
ValidateUserIdCmd::class,
// Messaging subcommands // Messaging subcommands
SignCmd::class, SignCmd::class,
VerifyCmd::class, VerifyCmd::class,
@ -79,8 +83,8 @@ class SopCLI {
// Re-set bundle with updated locale // Re-set bundle with updated locale
cliMsg = ResourceBundle.getBundle("msg_sop") cliMsg = ResourceBundle.getBundle("msg_sop")
return CommandLine(SopCLI::class.java) val cmd =
.apply { CommandLine(SopCLI::class.java).apply {
// explicitly set help command resource bundle // explicitly set help command resource bundle
subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help")) subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help"))
// Hide generate-completion command // Hide generate-completion command
@ -92,7 +96,15 @@ class SopCLI {
exitCodeExceptionMapper = SOPExceptionExitCodeMapper() exitCodeExceptionMapper = SOPExceptionExitCodeMapper()
isCaseInsensitiveEnumValuesAllowed = true isCaseInsensitiveEnumValuesAllowed = true
} }
.execute(*args)
// render Input/Output sections in help command
cmd.subcommands.values
.filter {
(it.getCommand() as Any) is AbstractSopCmd
} // Only for AbstractSopCmd objects
.forEach { (it.getCommand() as AbstractSopCmd).installIORenderer(it) }
return cmd.execute(*args)
} }
} }

View file

@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.cli.picocli
import java.util.*
import kotlin.system.exitProcess
import picocli.AutoComplete
import picocli.CommandLine
import sop.SOPV
import sop.cli.picocli.commands.*
import sop.exception.SOPGPException
@CommandLine.Command(
name = "sopv",
resourceBundle = "msg_sop",
exitCodeOnInvalidInput = SOPGPException.UnsupportedSubcommand.EXIT_CODE,
subcommands =
[
// Meta subcommands
VersionCmd::class,
// signature verification subcommands
VerifyCmd::class,
InlineVerifyCmd::class,
// misc
CommandLine.HelpCommand::class,
AutoComplete.GenerateCompletion::class])
class SopVCLI {
companion object {
@JvmStatic private var sopvInstance: SOPV? = null
@JvmStatic
fun getSopV(): SOPV =
checkNotNull(sopvInstance) { cliMsg.getString("sop.error.runtime.no_backend_set") }
@JvmStatic
fun setSopVInstance(sopv: SOPV?) {
sopvInstance = sopv
}
@JvmField var cliMsg: ResourceBundle = ResourceBundle.getBundle("msg_sop")
@JvmField var EXECUTABLE_NAME = "sopv"
@JvmField
@CommandLine.Option(names = ["--stacktrace"], scope = CommandLine.ScopeType.INHERIT)
var stacktrace = false
@JvmStatic
fun main(vararg args: String) {
val exitCode = execute(*args)
if (exitCode != 0) {
exitProcess(exitCode)
}
}
@JvmStatic
fun execute(vararg args: String): Int {
// Set locale
CommandLine(InitLocale()).parseArgs(*args)
// Re-set bundle with updated locale
cliMsg = ResourceBundle.getBundle("msg_sop")
return CommandLine(SopVCLI::class.java)
.apply {
// explicitly set help command resource bundle
subcommands["help"]?.setResourceBundle(ResourceBundle.getBundle("msg_help"))
// Hide generate-completion command
subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true)
// overwrite executable name
commandName = EXECUTABLE_NAME
// setup exception handling
executionExceptionHandler = SOPExecutionExceptionHandler()
exitCodeExceptionMapper = SOPExceptionExitCodeMapper()
isCaseInsensitiveEnumValuesAllowed = true
}
.execute(*args)
}
}
/**
* Control the locale.
*
* @see <a href="https://picocli.info/#_controlling_the_locale">Picocli Readme</a>
*/
@CommandLine.Command
class InitLocale {
@CommandLine.Option(names = ["-l", "--locale"], descriptionKey = "sop.locale")
fun setLocale(locale: String) = Locale.setDefault(Locale(locale))
@CommandLine.Unmatched
var remainder: MutableList<String> =
mutableListOf() // ignore any other parameters and options in the first parsing phase
}
}

View file

@ -7,6 +7,11 @@ package sop.cli.picocli.commands
import java.io.* import java.io.*
import java.text.ParseException import java.text.ParseException
import java.util.* import java.util.*
import picocli.CommandLine
import picocli.CommandLine.Help
import picocli.CommandLine.Help.Column
import picocli.CommandLine.Help.TextTable
import picocli.CommandLine.IHelpSectionRenderer
import sop.cli.picocli.commands.AbstractSopCmd.EnvironmentVariableResolver import sop.cli.picocli.commands.AbstractSopCmd.EnvironmentVariableResolver
import sop.exception.SOPGPException.* import sop.exception.SOPGPException.*
import sop.util.UTCUtil.Companion.parseUTCDate import sop.util.UTCUtil.Companion.parseUTCDate
@ -215,11 +220,106 @@ abstract class AbstractSopCmd(locale: Locale = Locale.getDefault()) : Runnable {
} }
} }
/**
* See
* [Example](https://github.com/remkop/picocli/blob/main/picocli-examples/src/main/java/picocli/examples/customhelp/EnvironmentVariablesSection.java)
*/
class InputOutputHelpSectionRenderer(private val argument: Pair<String?, String?>) :
IHelpSectionRenderer {
override fun render(help: Help): String {
return argument.let {
val calcLen =
help.calcLongOptionColumnWidth(
help.commandSpec().options(),
help.commandSpec().positionalParameters(),
help.colorScheme())
val keyLength =
help
.commandSpec()
.usageMessage()
.longOptionsMaxWidth()
.coerceAtMost(calcLen - 1)
val table =
TextTable.forColumns(
help.colorScheme(),
Column(keyLength + 7, 6, Column.Overflow.SPAN),
Column(width(help) - (keyLength + 7), 0, Column.Overflow.WRAP))
table.setAdjustLineBreaksForWideCJKCharacters(adjustCJK(help))
table.addRowValues("@|yellow ${argument.first}|@", argument.second ?: "")
table.toString()
}
}
private fun adjustCJK(help: Help) =
help.commandSpec().usageMessage().adjustLineBreaksForWideCJKCharacters()
private fun width(help: Help) = help.commandSpec().usageMessage().width()
}
fun installIORenderer(cmd: CommandLine) {
val inputName = getResString(cmd, "standardInput")
if (inputName != null) {
cmd.helpSectionMap[SECTION_KEY_STANDARD_INPUT_HEADING] = IHelpSectionRenderer {
getResString(cmd, "standardInputHeading")
}
cmd.helpSectionMap[SECTION_KEY_STANDARD_INPUT_DETAILS] =
InputOutputHelpSectionRenderer(
inputName to getResString(cmd, "standardInputDescription"))
cmd.helpSectionKeys =
insertKey(
cmd.helpSectionKeys,
SECTION_KEY_STANDARD_INPUT_HEADING,
SECTION_KEY_STANDARD_INPUT_DETAILS)
}
val outputName = getResString(cmd, "standardOutput")
if (outputName != null) {
cmd.helpSectionMap[SECTION_KEY_STANDARD_OUTPUT_HEADING] = IHelpSectionRenderer {
getResString(cmd, "standardOutputHeading")
}
cmd.helpSectionMap[SECTION_KEY_STANDARD_OUTPUT_DETAILS] =
InputOutputHelpSectionRenderer(
outputName to getResString(cmd, "standardOutputDescription"))
cmd.helpSectionKeys =
insertKey(
cmd.helpSectionKeys,
SECTION_KEY_STANDARD_OUTPUT_HEADING,
SECTION_KEY_STANDARD_OUTPUT_DETAILS)
}
}
private fun insertKey(keys: List<String>, header: String, details: String): List<String> {
val index =
keys.indexOf(CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST_HEADING)
val result = keys.toMutableList()
result.add(index, header)
result.add(index + 1, details)
return result
}
private fun getResString(cmd: CommandLine, key: String): String? =
try {
cmd.resourceBundle.getString(key)
} catch (m: MissingResourceException) {
try {
cmd.parent.resourceBundle.getString(key)
} catch (m: MissingResourceException) {
null
}
}
?.let { String.format(it) }
companion object { companion object {
const val PRFX_ENV = "@ENV:" const val PRFX_ENV = "@ENV:"
const val PRFX_FD = "@FD:" const val PRFX_FD = "@FD:"
const val SECTION_KEY_STANDARD_INPUT_HEADING = "standardInputHeading"
const val SECTION_KEY_STANDARD_INPUT_DETAILS = "standardInput"
const val SECTION_KEY_STANDARD_OUTPUT_HEADING = "standardOutputHeading"
const val SECTION_KEY_STANDARD_OUTPUT_DETAILS = "standardOutput"
@JvmField val DAWN_OF_TIME = Date(0) @JvmField val DAWN_OF_TIME = Date(0)
@JvmField @JvmField

View file

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

View file

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

View file

@ -19,8 +19,8 @@ class RevokeKeyCmd : AbstractSopCmd() {
@Option(names = ["--no-armor"], negatable = true) var armor = true @Option(names = ["--no-armor"], negatable = true) var armor = true
@Option(names = ["--with-key-password"], paramLabel = "PASSWORD") @Option(names = ["--with-key-password"], paramLabel = "PASSWORD", arity = "0..*")
var withKeyPassword: String? = null var withKeyPassword: List<String> = listOf()
override fun run() { override fun run() {
val revokeKey = throwIfUnsupportedSubcommand(SopCLI.getSop().revokeKey(), "revoke-key") val revokeKey = throwIfUnsupportedSubcommand(SopCLI.getSop().revokeKey(), "revoke-key")
@ -29,9 +29,9 @@ class RevokeKeyCmd : AbstractSopCmd() {
revokeKey.noArmor() revokeKey.noArmor()
} }
withKeyPassword?.let { for (passwordIn in withKeyPassword) {
try { try {
val password = stringFromInputStream(getInput(it)) val password = stringFromInputStream(getInput(passwordIn))
revokeKey.withKeyPassword(password) revokeKey.withKeyPassword(password)
} catch (e: SOPGPException.UnsupportedOption) { } catch (e: SOPGPException.UnsupportedOption) {
val errorMsg = val errorMsg =

View file

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

View file

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

View file

@ -22,6 +22,7 @@ class VersionCmd : AbstractSopCmd() {
@Option(names = ["--extended"]) var extended: Boolean = false @Option(names = ["--extended"]) var extended: Boolean = false
@Option(names = ["--backend"]) var backend: Boolean = false @Option(names = ["--backend"]) var backend: Boolean = false
@Option(names = ["--sop-spec"]) var sopSpec: Boolean = false @Option(names = ["--sop-spec"]) var sopSpec: Boolean = false
@Option(names = ["--sopv"]) var sopv: Boolean = false
} }
override fun run() { override fun run() {
@ -47,5 +48,10 @@ class VersionCmd : AbstractSopCmd() {
println(version.getSopSpecVersion()) println(version.getSopSpecVersion())
return return
} }
if (exclusive!!.sopv) {
println(version.getSopVVersion())
return
}
} }
} }

View file

@ -2,11 +2,14 @@
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
usage.header=Add ASCII Armor to standard input usage.header=Add ASCII Armor to standard input
label=Label to be used in the header and tail of the armoring
standardInput=BINARY
standardInputDescription=OpenPGP material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED)
standardOutput=ARMORED
standardOutputDescription=Same material, but with ASCII-armoring added, if not already present
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.optionListHeading=%nOptions:%n
usage.optionListHeading = %nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -2,11 +2,12 @@
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
usage.header=Schütze Standard-Eingabe mit ASCII Armor usage.header=Schütze Standard-Eingabe mit ASCII Armor
label=Label für Kopf- und Fußzeile der ASCII Armor
standardInputDescription=OpenPGP Material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED)
standardOutputDescription=Dasselbe Material, aber mit ASCII Armor kodiert, falls noch nicht geschehen
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.optionListHeading=%nOptionen:%n
usage.optionListHeading = %nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

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

View file

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

View file

@ -12,10 +12,15 @@ old-key-password.0=Old passwords to unlock the keys with.
old-key-password.1=Multiple passwords can be passed in, which are tested sequentially to unlock locked subkeys. old-key-password.1=Multiple passwords can be passed in, which are tested sequentially to unlock locked subkeys.
old-key-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). old-key-password.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
standardInput=KEYS
standardInputDescription=OpenPGP keys whose passphrases shall be changed
standardOutput=KEYS
standardOutputDescription=OpenPGP keys with changed passphrases
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nDescription:%n usage.descriptionHeading=%nDescription:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -12,10 +12,13 @@ old-key-password.0=Alte Passw
old-key-password.1=Mehrere Passwortkandidaten können übergeben werden, welche der Reihe nach durchprobiert werden, um Unterschlüssel zu entsperren. old-key-password.1=Mehrere Passwortkandidaten können übergeben werden, welche der Reihe nach durchprobiert werden, um Unterschlüssel zu entsperren.
old-key-password.2=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). old-key-password.2=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
standardInputDescription=OpenPGP Schlüssel deren Passwörter geändert werden sollen
standardOutputDescription=OpenPGP Schlüssel mit geänderten Passwörtern
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nBeschreibung:%n usage.descriptionHeading=%nBeschreibung:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -3,9 +3,14 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
usage.header=Remove ASCII Armor from standard input usage.header=Remove ASCII Armor from standard input
standardInput=ARMORED
standardInputDescription=Armored OpenPGP material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED)
standardOutput=BINARY
standardOutputDescription=Same material, but with ASCII-armoring removed
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -3,9 +3,12 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
usage.header=Entferne ASCII Armor von Standard-Eingabe usage.header=Entferne ASCII Armor von Standard-Eingabe
standardInputDescription=OpenPGP Material mit ASCII Armor (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED)
standardOutputDescription=Dasselbe Material, aber mit entfernter ASCII Armor
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -22,10 +22,15 @@ with-key-password.0=Passphrase to unlock the secret key(s).
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
KEY[0..*]=Secret keys to attempt decryption with KEY[0..*]=Secret keys to attempt decryption with
standardInput=CIPHERTEXT
standardInputDescription=Encrypted OpenPGP message
standardOutput=DATA
standardOutputDescription=Decrypted OpenPGP message
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -22,10 +22,13 @@ with-key-password.0=Passwort zum Entsperren der privaten Schl
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
KEY[0..*]=Private Schlüssel zum Entschlüsseln der Nachricht KEY[0..*]=Private Schlüssel zum Entschlüsseln der Nachricht
standardInputDescription=Verschlüsselte OpenPGP Nachricht
standardOutputDescription=Entschlüsselte OpenPGP Nachricht
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -11,10 +11,15 @@ with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, f
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). 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).
KEYS[0..*]=Secret keys used for signing KEYS[0..*]=Secret keys used for signing
standardInput=DATA
standardInputDescription=Data that shall be signed
standardOutput=SIGNATURES
standardOutputDescription=Detached OpenPGP signature(s)
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -11,10 +11,13 @@ with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable,
micalg-out=Gibt den verwendeten Digest-Algorithmus an die angegebene Ausgabe in einer Form aus, die zum Auffüllen des micalg-Parameters für den PGP/MIME Content-Type (RFC3156) verwendet werden kann. micalg-out=Gibt den verwendeten Digest-Algorithmus an die angegebene Ausgabe in einer Form aus, die zum Auffüllen des micalg-Parameters für den PGP/MIME Content-Type (RFC3156) verwendet werden kann.
KEYS[0..*]=Private Signaturschlüssel KEYS[0..*]=Private Signaturschlüssel
standardInputDescription=Daten die signiert werden sollen
standardOutputDescription=Abgetrennte OpenPGP Signatur(en)
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -13,11 +13,16 @@ not-after.3=Accepts special value "-" for end of time.
SIGNATURE[0]=Detached signature SIGNATURE[0]=Detached signature
CERT[1..*]=Public key certificates for signature verification CERT[1..*]=Public key certificates for signature verification
standardInput=DATA
standardInputDescription=Data over which the detached signatures were calculated
standardOutput=VERIFICATIONS
standardOutputDescription=Information about successfully verified signatures
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nDescription:%n usage.descriptionHeading=%nDescription:%n
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -13,11 +13,14 @@ not-after.3=Akzeptiert speziellen Wert '-' f
SIGNATURE[0]=Abgetrennte Signatur SIGNATURE[0]=Abgetrennte Signatur
CERT[1..*]=Zertifikate (öffentliche Schlüssel) zur Signaturprüfung CERT[1..*]=Zertifikate (öffentliche Schlüssel) zur Signaturprüfung
standardInputDescription=Daten, über die die abgetrennten Signaturen erstellt wurden
standardOutputDescription=Informationen über erfolgreich verifizierte Signaturen
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nBeschreibung:%n usage.descriptionHeading=%nBeschreibung:%n
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -12,10 +12,15 @@ with-key-password.0=Passphrase to unlock the secret key(s).
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
CERTS[0..*]=Certificates the message gets encrypted to CERTS[0..*]=Certificates the message gets encrypted to
standardInput=DATA
standardInputDescription=Data that shall be encrypted
standardOutput=CIPHERTEXT
standardOutputDescription=Encrypted OpenPGP message
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -12,10 +12,13 @@ with-key-password.0=Passwort zum Entsperren der privaten Schl
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
CERTS[0..*]=Zertifikate für die die Nachricht verschlüsselt werden soll CERTS[0..*]=Zertifikate für die die Nachricht verschlüsselt werden soll
standardInputDescription=Daten, die verschlüsselt werden sollen
standardOutputDescription=Verschlüsselte OpenPGP Nachricht
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -5,10 +5,15 @@ usage.header=Extract a public key certificate from a secret key
usage.description=Read a secret key from STDIN and emit the public key certificate to STDOUT. usage.description=Read a secret key from STDIN and emit the public key certificate to STDOUT.
no-armor=ASCII armor the output no-armor=ASCII armor the output
standardInput=KEYS
standardInputDescription=Private key(s), from which certificate(s) shall be extracted
standardOutput=CERTS
standardOutputDescription=Extracted certificate(s)
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nDescription:%n usage.descriptionHeading=%nDescription:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -5,10 +5,13 @@ usage.header=Extrahiere Zertifikat (
usage.description=Lese einen Schlüssel von Standard-Eingabe und gebe das Zertifikat auf Standard-Ausgabe aus. usage.description=Lese einen Schlüssel von Standard-Eingabe und gebe das Zertifikat auf Standard-Ausgabe aus.
no-armor=Schütze Ausgabe mit ASCII Armor no-armor=Schütze Ausgabe mit ASCII Armor
standardInputDescription=Private Schlüssel, deren Zertifikate extrahiert werden sollen
standardOutputDescription=Extrahierte Zertifikate
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nBeschreibung:%n usage.descriptionHeading=%nBeschreibung:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -9,10 +9,13 @@ signing-only=Generate a key that can only be used for signing
with-key-password.0=Password to protect the private key with with-key-password.0=Password to protect the private key with
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
standardOutput=KEYS
standardOutputDescription=Generated OpenPGP key
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -9,10 +9,12 @@ signing-only=Generiere einen Schl
with-key-password.0=Passwort zum Schutz des privaten Schlüssels with-key-password.0=Passwort zum Schutz des privaten Schlüssels
with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
standardOutputDescription=Erzeugter OpenPGP Schlüssel
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -6,6 +6,6 @@ usage.header=Display usage information for the specified subcommand
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -7,5 +7,5 @@ stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -5,9 +5,14 @@ usage.header=Split signatures from a clearsigned message
no-armor=ASCII armor the output no-armor=ASCII armor the output
signatures-out=Destination to which a detached signatures block will be written signatures-out=Destination to which a detached signatures block will be written
standardInput=INLINESIGNED
standardInputDescription=Inline-signed OpenPGP message
standardOutput=DATA
standardOutputDescription=The message without any signatures
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -5,9 +5,12 @@ usage.header=Trenne Signaturen von Klartext-signierter Nachricht
no-armor=Schütze Ausgabe mit ASCII Armor no-armor=Schütze Ausgabe mit ASCII Armor
signatures-out=Schreibe abgetrennte Signaturen in Ausgabe signatures-out=Schreibe abgetrennte Signaturen in Ausgabe
standardInputDescription=Klartext-signierte OpenPGP Nachricht
standardOutputDescription=Nachricht ohne Signaturen
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -13,10 +13,15 @@ with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, f
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). 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).
KEYS[0..*]=Secret keys used for signing KEYS[0..*]=Secret keys used for signing
standardInput=DATA
standardInputDescription=Data that shall be signed
standardOutput=INLINESIGNED
standardOutputDescription=Inline-signed OpenPGP message
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -13,10 +13,13 @@ with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable,
micalg=Gibt den verwendeten Digest-Algorithmus an die angegebene Ausgabe in einer Form aus, die zum Auffüllen des micalg-Parameters für den PGP/MIME Content-Type (RFC3156) verwendet werden kann. micalg=Gibt den verwendeten Digest-Algorithmus an die angegebene Ausgabe in einer Form aus, die zum Auffüllen des micalg-Parameters für den PGP/MIME Content-Type (RFC3156) verwendet werden kann.
KEYS[0..*]=Private Signaturschlüssel KEYS[0..*]=Private Signaturschlüssel
standardInputDescription=Daten, die signiert werden sollen
standardOutputDescription=Inline-signierte OpenPGP Nachricht
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -12,10 +12,15 @@ not-after.3=Accepts special value "-" for end of time.
verifications-out=File to write details over successful verifications to verifications-out=File to write details over successful verifications to
CERT[0..*]=Public key certificates for signature verification CERT[0..*]=Public key certificates for signature verification
standardInput=INLINESIGNED
standardInputDescription=Inline-signed OpenPGP message
standardOutput=DATA
standardOutputDescription=The message without any signatures
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -12,10 +12,13 @@ not-after.3=Akzeptiert speziellen Wert '-' f
verifications-out=Schreibe Status der Signaturprüfung in angegebene Ausgabe verifications-out=Schreibe Status der Signaturprüfung in angegebene Ausgabe
CERT[0..*]=Zertifikate (öffentlich Schlüssel) zur Signaturprüfung CERT[0..*]=Zertifikate (öffentlich Schlüssel) zur Signaturprüfung
standardInputDescription=Inline-signierte OpenPGP Nachricht
standardOutputDescription=Nachricht ohne Signaturen
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -4,10 +4,13 @@
usage.header=Emit a list of profiles supported by the identified subcommand usage.header=Emit a list of profiles supported by the identified subcommand
subcommand=Subcommand for which to list profiles subcommand=Subcommand for which to list profiles
standardOutput=PROFILELIST
standardOutputDescription=List of profiles supported by the identified subcommand
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -4,10 +4,12 @@
usage.header=Gebe eine Liste von Profilen aus, welche vom angegebenen Unterbefehl unterstützt werden usage.header=Gebe eine Liste von Profilen aus, welche vom angegebenen Unterbefehl unterstützt werden
subcommand=Unterbefehl, für welchen Profile gelistet werden sollen subcommand=Unterbefehl, für welchen Profile gelistet werden sollen
standardOutputDescription=Liste von Profilen, die der identifizierte Unterbefehl unterstützt
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.headerHeading=Merge OpenPGP certificates%n
usage.header=Merge OpenPGP certificates from standard input with related elements from CERTS and emit the result to standard output
usage.description=Only certificates that were part of standard input will be emitted to standard output
no-armor=ASCII armor the output
CERTS[0..*]=OpenPGP certificates from which updates shall be merged into the base certificates from standard input
standardInput=CERTS
standardInputDescription=Base certificates into which additional elements from the command line shall be merged
standardOutput=CERTS
standardOutputDescription=Merged certificates
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.descriptionHeading=%nNote:%n
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.headerHeading=OpenPGP Zertifikate zusammenführen%n
usage.header=Führe OpenPGP Zertifikate aus der Standardeingabe mit ensprechenden Elementen aus CERTS zusammen und gebe das Ergebnis auf der Standardausgabe aus
usage.description=Es werden nur Zertifikate auf die Standardausgabe geschrieben, welche Teil der Standardeingabe waren
no-armor=Schütze Ausgabe mit ASCII Armor
CERTS[0..*]=OpenPGP Zertifikate aus denen neue Elemente in die Basiszertifikate aus der Standardeingabe übernommen werden sollen
standardInputDescription=Basis-Zertifikate, in welche zusätzliche Elemente von der Kommandozeile zusammengeführt werden sollen
standardOutputDescription=Zusammengeführte Zertifikate
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.descriptionHeading=%nHinweis:%n
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -7,10 +7,15 @@ no-armor=ASCII armor the output
with-key-password.0=Passphrase to unlock the secret key(s). with-key-password.0=Passphrase to unlock the secret key(s).
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
standardInput=KEYS
standardInputDescription=OpenPGP key that shall be revoked
standardOutput=CERTS
standardOutputDescription=Revocation certificate
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nDescription:%n usage.descriptionHeading=D%nescription:%n
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -7,10 +7,13 @@ no-armor=Sch
with-key-password.0=Passwort zum Entsperren der privaten Schlüssel with-key-password.0=Passwort zum Entsperren der privaten Schlüssel
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
standardInputDescription=OpenPGP Schlüssel, der widerrufen werden soll
standardOutputDescription=Widerrufszertifikat
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.descriptionHeading=%nBeschreibung:%n usage.descriptionHeading=%nBeschreibung:%n
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -2,15 +2,21 @@
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
sop.name=sop sop.name=sop
sopv.name=sopv
usage.header=Stateless OpenPGP Protocol usage.header=Stateless OpenPGP Protocol
sopv.usage.header=Stateless OpenPGP Protocol - Signature Verification Interface Subset
locale=Locale for description texts locale=Locale for description texts
# Generic # Generic
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.parameterListHeading=%nParameters:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n
standardInputHeading=%nInput:%n
standardOutputHeading=%nOutput:%n
# Exit Codes # Exit Codes
usage.exitCodeListHeading=%nExit Codes:%n usage.exitCodeListHeading=%nExit Codes:%n
usage.exitCodeList.0=\u00200:Successful program execution usage.exitCodeList.0=\u00200:Successful program execution
@ -34,6 +40,10 @@ usage.exitCodeList.17=73:Ambiguous input (a filename matching the designator alr
usage.exitCodeList.18=79:Key is not signing capable usage.exitCodeList.18=79:Key is not signing capable
usage.exitCodeList.19=83:Options were supplied that are incompatible with each other usage.exitCodeList.19=83:Options were supplied that are incompatible with each other
usage.exitCodeList.20=89:The requested profile is unsupported, or the indicated subcommand does not accept profiles usage.exitCodeList.20=89:The requested profile is unsupported, or the indicated subcommand does not accept profiles
usage.exitCodeList.21=97:The implementation supports some form of hardware-backed secret keys, but could not identify the hardware device
usage.exitCodeList.22=101:The implementation tried to use a hardware-backed secret key, but the cryptographic hardware refused the operation for some reason other than a bad PIN or password
usage.exitCodeList.23=103:The primary key of a KEYS object is too weak or revoked
usage.exitCodeList.24=107:The CERTS object has no matching User ID
## SHARED RESOURCES ## SHARED RESOURCES
stacktrace=Print stacktrace stacktrace=Print stacktrace
@ -70,6 +80,8 @@ sop.error.runtime.cert_cannot_encrypt=Certificate from input '%s' cannot encrypt
sop.error.runtime.no_session_key_extracted=Session key not extracted. Feature potentially not supported. sop.error.runtime.no_session_key_extracted=Session key not extracted. Feature potentially not supported.
sop.error.runtime.no_verifiable_signature_found=No verifiable signature found. sop.error.runtime.no_verifiable_signature_found=No verifiable signature found.
sop.error.runtime.cannot_decrypt_message=Message could not be decrypted. sop.error.runtime.cannot_decrypt_message=Message could not be decrypted.
sop.error.runtime.cert_user_id_no_match=Certificate '%s' does not contain a valid binding for user id '%s'.
sop.error.runtime.any_cert_user_id_no_match=Any certificate does not contain a valid binding for user id '%s'.
## Usage errors ## Usage errors
sop.error.usage.password_or_cert_required=At least one password file or cert file required for encryption. sop.error.usage.password_or_cert_required=At least one password file or cert file required for encryption.
sop.error.usage.argument_required=Argument '%s' is required. sop.error.usage.argument_required=Argument '%s' is required.

View file

@ -2,15 +2,21 @@
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
sop.name=sop sop.name=sop
sopv.name=sopv
usage.header=Stateless OpenPGP Protocol usage.header=Stateless OpenPGP Protocol
sopv.usage.header=Stateless OpenPGP Protocol - Signature Verification Interface Subset
locale=Gebietsschema für Beschreibungstexte locale=Gebietsschema für Beschreibungstexte
# Generic # Generic
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.parameterListHeading=%nParameter:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n
standardInputHeading=%nEingabe:%n
standardOutputHeading=%nAusgabe:%n
# Exit Codes # Exit Codes
usage.exitCodeListHeading=%nExit Codes:%n usage.exitCodeListHeading=%nExit Codes:%n
usage.exitCodeList.0=\u00200:Erfolgreiche Programmausführung usage.exitCodeList.0=\u00200:Erfolgreiche Programmausführung
@ -34,6 +40,10 @@ usage.exitCodeList.17=73:Mehrdeutige Eingabe (ein Dateiname, der dem Bezeichner
usage.exitCodeList.18=79:Schlüssel ist nicht fähig zu signieren usage.exitCodeList.18=79:Schlüssel ist nicht fähig zu signieren
usage.exitCodeList.19=83:Miteinander inkompatible Optionen spezifiziert usage.exitCodeList.19=83:Miteinander inkompatible Optionen spezifiziert
usage.exitCodeList.20=89:Das angeforderte Profil wird nicht unterstützt, oder der angegebene Unterbefehl akzeptiert keine Profile usage.exitCodeList.20=89:Das angeforderte Profil wird nicht unterstützt, oder der angegebene Unterbefehl akzeptiert keine Profile
usage.exitCodeList.21=97:Die Anwendung unterstützt hardwaregestützte private Schlüssel, aber kann das Gerät nicht identifizieren
usage.exitCodeList.22=101:Die Anwendung versuchte, einen hardwaregestützten Schlüssel zu verwenden, aber das Gerät lehnte den Vorgang aus einem anderen Grund als einer falschen PIN oder einem falschen Passwort ab
usage.exitCodeList.23=103:Der primäre private Schlüssel ist zu schwach oder widerrufen
usage.exitCodeList.24=107:Das Zertifikat hat keine übereinstimmende User ID
## SHARED RESOURCES ## SHARED RESOURCES
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben

View file

@ -0,0 +1,24 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Keep a secret key up-to-date
no-armor=ASCII armor the output
signing-only=TODO: Document
no-new-mechanisms=Do not add feature support for new mechanisms, which the key did not previously support
with-key-password.0=Passphrase to unlock the secret key(s).
with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...).
merge-certs.0=Merge additional elements found in the corresponding CERTS objects into the updated secret keys
merge-certs.1=This can be used, for example, to absorb a third-party certification into the Transferable Secret Key
standardInput=KEYS
standardInputDescription=OpenPGP key that shall be kept up-to-date
standardOutput=KEYS
standardOutputDescription=Updated OpenPGP key
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -0,0 +1,21 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Halte einen Schlüssel auf dem neusten Stand
no-armor=Schütze Ausgabe mit ASCII Armor
signing-only=TODO: Dokumentieren
no-new-mechanisms=Füge keine neuen Funktionen hinzu, die der Schlüssel nicht bereits zuvor unterstützt hat
with-key-password.0=Passwort zum Entsperren der privaten Schlüssel
with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...).
merge-certs.0=Führe zusätzliche Elemente aus entsprechenden CERTS Objekten mit dem privaten Schlüssel zusammen
merge-certs.1=Dies kann zum Beispiel dazu genutzt werden, Zertifizierungen dritter in den privaten Schlüssel zu übernehmen
standardInputDescription=OpenPGP Schlüssel, der auf den neusten Stand gebracht werden soll
standardOutputDescription=Erneuerter OpenPGP Schlüssel
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Validate a UserID in an OpenPGP certificate
addr-spec-only=Treat the USERID as an email address, match only against the email address part of each correctly bound UserID
USERID[0]=UserID
CERTS[1..*]=Authority OpenPGP certificates
standardInput=CERTS
standardInputDescription=OpenPGP certificates in which UserID bindings shall be validated
picocli.endofoptions.description=End of options. Remainder are positional parameters. Fixes 'Missing required parameter' error
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameters:%n
usage.synopsisHeading=Usage:\u0020
usage.commandListHeading=%nCommands:%n
usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n

View file

@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
#
# SPDX-License-Identifier: Apache-2.0
usage.header=Validiere eine UserID auf OpenPGP Zertifikaten
addr-spec-only=Behandle die USERID als E-Mail-Adresse, vergleiche sie nur mit dem E-Mail-Adressen-Teil jeder korrekten UserID
USERID[0]=UserID
CERTS[1..*]=Autoritäre OpenPGP Zertifikate
standardInput=CERTS
standardInputDescription=OpenPGP Zertifikate auf denen UserIDs validiert werden sollen
picocli.endofoptions.description=Ende der Optionen. Der Rest sind Positionsparameter. Behebt 'Missing required parameter' Fehler
stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0
usage.parameterListHeading=%nParameter:%n
usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n
usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n

View file

@ -6,9 +6,11 @@ extended=Print an extended version string
backend=Print information about the cryptographic backend backend=Print information about the cryptographic backend
sop-spec=Print the latest revision of the SOP specification targeted by the implementation sop-spec=Print the latest revision of the SOP specification targeted by the implementation
standardOutput=version information
stacktrace=Print stacktrace stacktrace=Print stacktrace
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Usage:\u0020 usage.synopsisHeading=Usage:\u0020
usage.commandListHeading = %nCommands:%n usage.commandListHeading=%nCommands:%n
usage.optionListHeading = %nOptions:%n usage.optionListHeading=%nOptions:%n
usage.footerHeading=Powered by picocli%n usage.footerHeading=Powered by picocli%n

View file

@ -6,9 +6,11 @@ extended=Gebe erweiterte Versionsinformationen aus
backend=Gebe Informationen über das kryptografische Backend aus backend=Gebe Informationen über das kryptografische Backend aus
sop-spec=Gebe die neuste Revision der SOP Spezifikation aus, welche von dieser Implementierung umgesetzt wird sop-spec=Gebe die neuste Revision der SOP Spezifikation aus, welche von dieser Implementierung umgesetzt wird
standardOutput=Versionsinformationen
stacktrace=Stacktrace ausgeben stacktrace=Stacktrace ausgeben
# Generic TODO: Remove when bumping picocli to 4.7.0 # Generic TODO: Remove when bumping picocli to 4.7.0
usage.synopsisHeading=Aufruf:\u0020 usage.synopsisHeading=Aufruf:\u0020
usage.commandListHeading=%nBefehle:%n usage.commandListHeading=%nBefehle:%n
usage.optionListHeading = %nOptionen:%n usage.optionListHeading=%nOptionen:%n
usage.footerHeading=Powered by Picocli%n usage.footerHeading=Powered by Picocli%n

View file

@ -6,16 +6,18 @@ package sop.cli.picocli;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError;
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedSubcommand;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sop.SOP; import sop.SOP;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.operation.Armor; import sop.operation.Armor;
import sop.operation.CertifyUserId;
import sop.operation.ChangeKeyPassword; import sop.operation.ChangeKeyPassword;
import sop.operation.Dearmor; import sop.operation.Dearmor;
import sop.operation.Decrypt; import sop.operation.Decrypt;
@ -28,31 +30,52 @@ import sop.operation.InlineVerify;
import sop.operation.DetachedSign; import sop.operation.DetachedSign;
import sop.operation.DetachedVerify; import sop.operation.DetachedVerify;
import sop.operation.ListProfiles; import sop.operation.ListProfiles;
import sop.operation.MergeCerts;
import sop.operation.RevokeKey; import sop.operation.RevokeKey;
import sop.operation.UpdateKey;
import sop.operation.ValidateUserId;
import sop.operation.Version; import sop.operation.Version;
public class SOPTest { public class SOPTest {
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedSubcommand.EXIT_CODE)
public void assertExitOnInvalidSubcommand() { public void assertExitOnInvalidSubcommand() {
SOP sop = mock(SOP.class); SOP sop = mock(SOP.class);
SopCLI.setSopInstance(sop); SopCLI.setSopInstance(sop);
SopCLI.main(new String[] {"invalid"}); assertUnsupportedSubcommand(() -> SopCLI.execute("invalid"));
} }
@Test @Test
@ExpectSystemExitWithStatus(1)
public void assertThrowsIfNoSOPBackendSet() { public void assertThrowsIfNoSOPBackendSet() {
SopCLI.setSopInstance(null); SopCLI.setSopInstance(null);
// At this point, no SOP backend is set, so an InvalidStateException triggers exit(1) // At this point, no SOP backend is set, so an InvalidStateException triggers error code 1
SopCLI.main(new String[] {"armor"}); assertGenericError(() -> SopCLI.execute("armor"));
} }
@Test @Test
public void UnsupportedSubcommandsTest() { public void UnsupportedSubcommandsTest() {
SOP nullCommandSOP = new SOP() { SOP nullCommandSOP = new SOP() {
@Override
public ValidateUserId validateUserId() {
return null;
}
@Override
public CertifyUserId certifyUserId() {
return null;
}
@Override
public MergeCerts mergeCerts() {
return null;
}
@Override
public UpdateKey updateKey() {
return null;
}
@Override @Override
public Version version() { public Version version() {
return null; return null;
@ -141,6 +164,11 @@ public class SOPTest {
commands.add(new String[] {"sign"}); commands.add(new String[] {"sign"});
commands.add(new String[] {"verify", "signature.asc", "cert.asc"}); commands.add(new String[] {"verify", "signature.asc", "cert.asc"});
commands.add(new String[] {"version"}); commands.add(new String[] {"version"});
commands.add(new String[] {"list-profiles", "generate-key"});
commands.add(new String[] {"certify-userid", "--userid", "Alice <alice@pgpainless.org>", "--", "alice.pgp"});
commands.add(new String[] {"validate-userid", "Alice <alice@pgpainless.org>", "bob.pgp", "--", "alice.pgp"});
commands.add(new String[] {"update-key"});
commands.add(new String[] {"merge-certs"});
for (String[] command : commands) { for (String[] command : commands) {
int exit = SopCLI.execute(command); int exit = SopCLI.execute(command);

View file

@ -4,8 +4,6 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import com.ginsberg.junit.exit.FailOnSystemExit;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sop.Ready; import sop.Ready;
@ -24,6 +22,8 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
public class ArmorCmdTest { public class ArmorCmdTest {
@ -42,24 +42,22 @@ public class ArmorCmdTest {
@Test @Test
public void assertDataIsAlwaysCalled() throws SOPGPException.BadData, IOException { public void assertDataIsAlwaysCalled() throws SOPGPException.BadData, IOException {
SopCLI.main(new String[] {"armor"}); assertSuccess(() -> SopCLI.execute("armor"));
verify(armor, times(1)).data((InputStream) any()); verify(armor, times(1)).data((InputStream) any());
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void ifBadDataExit41() throws SOPGPException.BadData, IOException { public void ifBadDataExit41() throws SOPGPException.BadData, IOException {
when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"armor"}); assertBadData(() -> SopCLI.execute("armor"));
} }
@Test @Test
@FailOnSystemExit
public void ifNoErrorsNoExit() { public void ifNoErrorsNoExit() {
when(sop.armor()).thenReturn(armor); when(sop.armor()).thenReturn(armor);
SopCLI.main(new String[] {"armor"}); assertSuccess(() -> SopCLI.execute("armor"));
} }
private static Ready nopReady() { private static Ready nopReady() {

View file

@ -9,12 +9,13 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sop.Ready; import sop.Ready;
@ -48,14 +49,13 @@ public class DearmorCmdTest {
@Test @Test
public void assertDataIsCalled() throws IOException, SOPGPException.BadData { public void assertDataIsCalled() throws IOException, SOPGPException.BadData {
SopCLI.main(new String[] {"dearmor"}); assertSuccess(() -> SopCLI.execute("dearmor"));
verify(dearmor, times(1)).data((InputStream) any()); verify(dearmor, times(1)).data((InputStream) any());
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void assertBadDataCausesExit41() throws IOException, SOPGPException.BadData { public void assertBadDataCausesExit41() throws IOException, SOPGPException.BadData {
when(dearmor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException("invalid armor"))); when(dearmor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException("invalid armor")));
SopCLI.main(new String[] {"dearmor"}); assertBadData(() -> SopCLI.execute("dearmor"));
} }
} }

View file

@ -4,7 +4,6 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatcher;
@ -42,6 +41,18 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
import static sop.testsuite.assertions.SopExecutionAssertions.assertCannotDecrypt;
import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError;
import static sop.testsuite.assertions.SopExecutionAssertions.assertIncompleteVerification;
import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyIsProtected;
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg;
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput;
import static sop.testsuite.assertions.SopExecutionAssertions.assertOutputExists;
import static sop.testsuite.assertions.SopExecutionAssertions.assertPasswordNotHumanReadable;
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedAsymmetricAlgo;
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption;
public class DecryptCmdTest { public class DecryptCmdTest {
@ -74,47 +85,47 @@ public class DecryptCmdTest {
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE)
public void missingArgumentsExceptionCausesExit19() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException { public void missingArgumentsExceptionCausesExit19() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException {
when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.MissingArg("Missing arguments.")); when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.MissingArg("Missing arguments."));
SopCLI.main(new String[] {"decrypt"}); assertMissingArg(() -> SopCLI.execute("decrypt"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void badDataExceptionCausesExit41() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException { public void badDataExceptionCausesExit41() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException {
when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"decrypt"}); assertBadData(() -> SopCLI.execute("decrypt"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.PasswordNotHumanReadable.EXIT_CODE)
public void assertNotHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, public void assertNotHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable,
SOPGPException.UnsupportedOption, IOException { SOPGPException.UnsupportedOption, IOException {
File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable"); File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable");
when(decrypt.withPassword(any())).thenThrow(new SOPGPException.PasswordNotHumanReadable()); when(decrypt.withPassword(any())).thenThrow(new SOPGPException.PasswordNotHumanReadable());
SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); assertPasswordNotHumanReadable(() ->
SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath())
);
} }
@Test @Test
public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
File passwordFile = TestFileUtil.writeTempStringFile("orange"); File passwordFile = TestFileUtil.writeTempStringFile("orange");
SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); assertSuccess(() -> SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath()));
verify(decrypt, times(1)).withPassword("orange"); verify(decrypt, times(1)).withPassword("orange");
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
File passwordFile = TestFileUtil.writeTempStringFile("swordfish"); File passwordFile = TestFileUtil.writeTempStringFile("swordfish");
when(decrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Decrypting with password not supported.")); when(decrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Decrypting with password not supported."));
SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); assertUnsupportedOption(() ->
SopCLI.execute("decrypt", "--with-password", passwordFile.getAbsolutePath())
);
} }
@Test @Test
public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption { public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption {
Date now = new Date(); Date now = new Date();
SopCLI.main(new String[] {"decrypt"}); assertSuccess(() -> SopCLI.execute("decrypt"));
verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME); verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME);
verify(decrypt, times(1)).verifyNotAfter( verify(decrypt, times(1)).verifyNotAfter(
ArgumentMatchers.argThat(argument -> { ArgumentMatchers.argThat(argument -> {
@ -125,7 +136,8 @@ public class DecryptCmdTest {
@Test @Test
public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption { public void assertVerifyNotAfterAndBeforeDashResultsInMaxTimeRange() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"decrypt", "--verify-not-before", "-", "--verify-not-after", "-"}); assertSuccess(() ->
SopCLI.execute("decrypt", "--verify-not-before", "-", "--verify-not-after", "-"));
verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME); verify(decrypt, times(1)).verifyNotBefore(AbstractSopCmd.BEGINNING_OF_TIME);
verify(decrypt, times(1)).verifyNotAfter(AbstractSopCmd.END_OF_TIME); verify(decrypt, times(1)).verifyNotAfter(AbstractSopCmd.END_OF_TIME);
} }
@ -138,54 +150,57 @@ public class DecryptCmdTest {
return Math.abs(now.getTime() - argument.getTime()) <= 1000; return Math.abs(now.getTime() - argument.getTime()) <= 1000;
}; };
SopCLI.main(new String[] {"decrypt", "--verify-not-before", "now", "--verify-not-after", "now"}); assertSuccess(() ->
SopCLI.execute("decrypt", "--verify-not-before", "now", "--verify-not-after", "now"));
verify(decrypt, times(1)).verifyNotAfter(ArgumentMatchers.argThat(isMaxOneSecOff)); verify(decrypt, times(1)).verifyNotAfter(ArgumentMatchers.argThat(isMaxOneSecOff));
verify(decrypt, times(1)).verifyNotBefore(ArgumentMatchers.argThat(isMaxOneSecOff)); verify(decrypt, times(1)).verifyNotBefore(ArgumentMatchers.argThat(isMaxOneSecOff));
} }
@Test @Test
@ExpectSystemExitWithStatus(1)
public void assertMalformedDateInNotBeforeCausesExit1() { public void assertMalformedDateInNotBeforeCausesExit1() {
// ParserException causes exit(1) // ParserException causes exit(1)
SopCLI.main(new String[] {"decrypt", "--verify-not-before", "invalid"}); assertGenericError(() ->
SopCLI.execute("decrypt", "--verify-not-before", "invalid"));
} }
@Test @Test
@ExpectSystemExitWithStatus(1)
public void assertMalformedDateInNotAfterCausesExit1() { public void assertMalformedDateInNotAfterCausesExit1() {
// ParserException causes exit(1) // ParserException causes exit(1)
SopCLI.main(new String[] {"decrypt", "--verify-not-after", "invalid"}); assertGenericError(() ->
SopCLI.execute("decrypt", "--verify-not-after", "invalid"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void assertUnsupportedNotAfterCausesExit37() throws SOPGPException.UnsupportedOption { public void assertUnsupportedNotAfterCausesExit37() throws SOPGPException.UnsupportedOption {
when(decrypt.verifyNotAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); when(decrypt.verifyNotAfter(any())).thenThrow(
SopCLI.main(new String[] {"decrypt", "--verify-not-after", "now"}); new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported."));
assertUnsupportedOption(() ->
SopCLI.execute("decrypt", "--verify-not-after", "now"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void assertUnsupportedNotBeforeCausesExit37() throws SOPGPException.UnsupportedOption { public void assertUnsupportedNotBeforeCausesExit37() throws SOPGPException.UnsupportedOption {
when(decrypt.verifyNotBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); when(decrypt.verifyNotBefore(any())).thenThrow(
SopCLI.main(new String[] {"decrypt", "--verify-not-before", "now"}); new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported."));
assertUnsupportedOption(() ->
SopCLI.execute("decrypt", "--verify-not-before", "now"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE)
public void assertExistingSessionKeyOutFileCausesExit59() throws IOException { public void assertExistingSessionKeyOutFileCausesExit59() throws IOException {
File tempFile = File.createTempFile("existing-session-key-", ".tmp"); File tempFile = File.createTempFile("existing-session-key-", ".tmp");
tempFile.deleteOnExit(); tempFile.deleteOnExit();
SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); assertOutputExists(() ->
SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void assertWhenSessionKeyCannotBeExtractedExit37() throws IOException { public void assertWhenSessionKeyCannotBeExtractedExit37() throws IOException {
Path tempDir = Files.createTempDirectory("session-key-out-dir"); Path tempDir = Files.createTempDirectory("session-key-out-dir");
File tempFile = new File(tempDir.toFile(), "session-key"); File tempFile = new File(tempDir.toFile(), "session-key");
tempFile.deleteOnExit(); tempFile.deleteOnExit();
SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); assertUnsupportedOption(() ->
SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath()));
} }
@Test @Test
@ -210,8 +225,10 @@ public class DecryptCmdTest {
File verificationsFile = new File(tempDir.toFile(), "verifications"); File verificationsFile = new File(tempDir.toFile(), "verifications");
File keyFile = new File(tempDir.toFile(), "key.asc"); File keyFile = new File(tempDir.toFile(), "key.asc");
keyFile.createNewFile(); keyFile.createNewFile();
SopCLI.main(new String[] {"decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(), assertSuccess(() ->
"--verifications-out", verificationsFile.getAbsolutePath(), "--verify-with", keyFile.getAbsolutePath()}); SopCLI.execute("decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(),
"--verifications-out", verificationsFile.getAbsolutePath(), "--verify-with",
keyFile.getAbsolutePath()));
ByteArrayOutputStream bytesInFile = new ByteArrayOutputStream(); ByteArrayOutputStream bytesInFile = new ByteArrayOutputStream();
try (FileInputStream fileIn = new FileInputStream(sessionKeyFile)) { try (FileInputStream fileIn = new FileInputStream(sessionKeyFile)) {
@ -241,10 +258,10 @@ public class DecryptCmdTest {
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.CannotDecrypt.EXIT_CODE)
public void assertUnableToDecryptExceptionResultsInExit29() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { public void assertUnableToDecryptExceptionResultsInExit29() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException {
when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.CannotDecrypt()); when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.CannotDecrypt());
SopCLI.main(new String[] {"decrypt"}); assertCannotDecrypt(() ->
SopCLI.execute("decrypt"));
} }
@Test @Test
@ -258,30 +275,32 @@ public class DecryptCmdTest {
return new DecryptionResult(null, Collections.emptyList()); return new DecryptionResult(null, Collections.emptyList());
} }
}); });
SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath(), "--verifications-out", verifyOut.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("decrypt", "--verify-with", tempFile.getAbsolutePath(), "--verifications-out",
verifyOut.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void badDataInVerifyWithCausesExit41() throws IOException, SOPGPException.BadData { public void badDataInVerifyWithCausesExit41() throws IOException, SOPGPException.BadData {
when(decrypt.verifyWithCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(decrypt.verifyWithCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
File tempFile = File.createTempFile("verify-with-", ".tmp"); File tempFile = File.createTempFile("verify-with-", ".tmp");
SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath()}); assertBadData(() ->
SopCLI.execute("decrypt", "--verify-with", tempFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
public void unexistentCertFileCausesExit61() { public void unexistentCertFileCausesExit61() {
SopCLI.main(new String[] {"decrypt", "--verify-with", "invalid"}); assertMissingInput(() ->
SopCLI.execute("decrypt", "--verify-with", "invalid"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE)
public void existingVerifyOutCausesExit59() throws IOException { public void existingVerifyOutCausesExit59() throws IOException {
File certFile = File.createTempFile("existing-verify-out-cert", ".asc"); File certFile = File.createTempFile("existing-verify-out-cert", ".asc");
File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp"); File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp");
SopCLI.main(new String[] {"decrypt", "--verifications-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); assertOutputExists(() -> SopCLI.execute("decrypt", "--verifications-out",
existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()));
} }
@Test @Test
@ -305,7 +324,9 @@ public class DecryptCmdTest {
} }
}); });
SopCLI.main(new String[] {"decrypt", "--verifications-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("decrypt", "--verifications-out", verifyOut.getAbsolutePath(),
"--verify-with", certFile.getAbsolutePath()));
try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) { try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) {
String line = reader.readLine(); String line = reader.readLine();
assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line); assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line);
@ -320,66 +341,64 @@ public class DecryptCmdTest {
File sessionKeyFile1 = TestFileUtil.writeTempStringFile(key1.toString()); File sessionKeyFile1 = TestFileUtil.writeTempStringFile(key1.toString());
File sessionKeyFile2 = TestFileUtil.writeTempStringFile(key2.toString()); File sessionKeyFile2 = TestFileUtil.writeTempStringFile(key2.toString());
SopCLI.main(new String[] {"decrypt", assertSuccess(() ->
"--with-session-key", sessionKeyFile1.getAbsolutePath(), SopCLI.execute("decrypt",
"--with-session-key", sessionKeyFile2.getAbsolutePath()}); "--with-session-key", sessionKeyFile1.getAbsolutePath(),
"--with-session-key", sessionKeyFile2.getAbsolutePath()));
verify(decrypt).withSessionKey(key1); verify(decrypt).withSessionKey(key1);
verify(decrypt).withSessionKey(key2); verify(decrypt).withSessionKey(key2);
} }
@Test @Test
@ExpectSystemExitWithStatus(1)
public void assertMalformedSessionKeysResultInExit1() throws IOException { public void assertMalformedSessionKeysResultInExit1() throws IOException {
File sessionKeyFile = TestFileUtil.writeTempStringFile("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137"); File sessionKeyFile = TestFileUtil.writeTempStringFile("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137");
SopCLI.main(new String[] {"decrypt", assertGenericError(() ->
"--with-session-key", sessionKeyFile.getAbsolutePath()}); SopCLI.execute("decrypt",
"--with-session-key", sessionKeyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void assertBadDataInKeysResultsInExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { public void assertBadDataInKeysResultsInExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException {
when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
File tempKeyFile = File.createTempFile("key-", ".tmp"); File tempKeyFile = File.createTempFile("key-", ".tmp");
SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); assertBadData(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
public void assertKeyFileNotFoundCausesExit61() { public void assertKeyFileNotFoundCausesExit61() {
SopCLI.main(new String[] {"decrypt", "nonexistent-key"}); assertMissingInput(() -> SopCLI.execute("decrypt", "nonexistent-key"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE)
public void assertProtectedKeyCausesExit67() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { public void assertProtectedKeyCausesExit67() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
File tempKeyFile = File.createTempFile("key-", ".tmp"); File tempKeyFile = File.createTempFile("key-", ".tmp");
SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); assertKeyIsProtected(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE)
public void assertUnsupportedAlgorithmExceptionCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { public void assertUnsupportedAlgorithmExceptionCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException {
when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new IOException())); when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new IOException()));
File tempKeyFile = File.createTempFile("key-", ".tmp"); File tempKeyFile = File.createTempFile("key-", ".tmp");
SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); assertUnsupportedAsymmetricAlgo(() ->
SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
public void assertMissingPassphraseFileCausesExit61() { public void assertMissingPassphraseFileCausesExit61() {
SopCLI.main(new String[] {"decrypt", "--with-password", "missing"}); assertMissingInput(() ->
SopCLI.execute("decrypt", "--with-password", "missing"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
public void assertMissingSessionKeyFileCausesExit61() { public void assertMissingSessionKeyFileCausesExit61() {
SopCLI.main(new String[] {"decrypt", "--with-session-key", "missing"}); assertMissingInput(() ->
SopCLI.execute("decrypt", "--with-session-key", "missing"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.IncompleteVerification.EXIT_CODE)
public void verifyOutWithoutVerifyWithCausesExit23() { public void verifyOutWithoutVerifyWithCausesExit23() {
SopCLI.main(new String[] {"decrypt", "--verifications-out", "out.file"}); assertIncompleteVerification(() ->
SopCLI.execute("decrypt", "--verifications-out", "out.file"));
} }
} }

View file

@ -4,7 +4,6 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -28,6 +27,17 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
import static sop.testsuite.assertions.SopExecutionAssertions.assertCertCannotEncrypt;
import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError;
import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyCannotSign;
import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyIsProtected;
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg;
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput;
import static sop.testsuite.assertions.SopExecutionAssertions.assertPasswordNotHumanReadable;
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedAsymmetricAlgo;
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption;
public class EncryptCmdTest { public class EncryptCmdTest {
@ -50,48 +60,50 @@ public class EncryptCmdTest {
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void missingBothPasswordAndCertFileCausesMissingArg() {
public void missingBothPasswordAndCertFileCauseExit19() { assertMissingArg(() ->
SopCLI.main(new String[] {"encrypt", "--no-armor"}); SopCLI.execute("encrypt", "--no-armor"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void as_unsupportedEncryptAsCausesUnsupportedOption() throws SOPGPException.UnsupportedOption {
public void as_unsupportedEncryptAsCausesExit37() throws SOPGPException.UnsupportedOption {
when(encrypt.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting encryption mode not supported.")); when(encrypt.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting encryption mode not supported."));
SopCLI.main(new String[] {"encrypt", "--as", "Binary"}); assertUnsupportedOption(() ->
SopCLI.execute("encrypt", "--as", "Binary"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void as_invalidModeOptionCausesUnsupportedOption() {
public void as_invalidModeOptionCausesExit37() { assertUnsupportedOption(() ->
SopCLI.main(new String[] {"encrypt", "--as", "invalid"}); SopCLI.execute("encrypt", "--as", "invalid"));
} }
@Test @Test
public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption, IOException { public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption, IOException {
File passwordFile = TestFileUtil.writeTempStringFile("0rbit"); File passwordFile = TestFileUtil.writeTempStringFile("0rbit");
for (EncryptAs mode : EncryptAs.values()) { for (EncryptAs mode : EncryptAs.values()) {
SopCLI.main(new String[] {"encrypt", "--as", mode.name(), "--with-password", passwordFile.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("encrypt", "--as", mode.name(),
"--with-password", passwordFile.getAbsolutePath()));
verify(encrypt, times(1)).mode(mode); verify(encrypt, times(1)).mode(mode);
} }
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.PasswordNotHumanReadable.EXIT_CODE) public void withPassword_notHumanReadablePasswordCausesPWNotHumanReadable() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
public void withPassword_notHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable()); when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable());
File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable"); File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable");
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); assertPasswordNotHumanReadable(() ->
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void withPassword_unsupportedWithPasswordCausesUnsupportedOption() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
public void withPassword_unsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException {
when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported.")); when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported."));
File passwordFile = TestFileUtil.writeTempStringFile("orange"); File passwordFile = TestFileUtil.writeTempStringFile("orange");
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); assertUnsupportedOption(() ->
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath()));
} }
@Test @Test
@ -99,99 +111,107 @@ public class EncryptCmdTest {
File keyFile1 = File.createTempFile("sign-with-1-", ".asc"); File keyFile1 = File.createTempFile("sign-with-1-", ".asc");
File keyFile2 = File.createTempFile("sign-with-2-", ".asc"); File keyFile2 = File.createTempFile("sign-with-2-", ".asc");
File passwordFile = TestFileUtil.writeTempStringFile("password"); File passwordFile = TestFileUtil.writeTempStringFile("password");
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile1.getAbsolutePath(), "--sign-with", keyFile2.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(),
"--sign-with", keyFile1.getAbsolutePath(),
"--sign-with", keyFile2.getAbsolutePath()));
verify(encrypt, times(2)).signWith((InputStream) any()); verify(encrypt, times(2)).signWith((InputStream) any());
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void signWith_nonExistentKeyFileCausesMissingInput() {
public void signWith_nonExistentKeyFileCausesExit61() { assertMissingInput(() ->
SopCLI.main(new String[] {"encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"}); SopCLI.execute("encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE) public void signWith_keyIsProtectedCausesKeyIsProtected() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
File keyFile = File.createTempFile("sign-with", ".asc"); File keyFile = File.createTempFile("sign-with", ".asc");
File passwordFile = TestFileUtil.writeTempStringFile("starship"); File passwordFile = TestFileUtil.writeTempStringFile("starship");
SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", passwordFile.getAbsolutePath()}); assertKeyIsProtected(() ->
SopCLI.execute("encrypt", "--sign-with", keyFile.getAbsolutePath(),
"--with-password", passwordFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) public void signWith_unsupportedAsymmetricAlgoCausesUnsupportedAsymAlgo() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception()));
File keyFile = File.createTempFile("sign-with", ".asc"); File keyFile = File.createTempFile("sign-with", ".asc");
File passwordFile = TestFileUtil.writeTempStringFile("123456"); File passwordFile = TestFileUtil.writeTempStringFile("123456");
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); assertUnsupportedAsymmetricAlgo(() ->
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(),
"--sign-with", keyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.KeyCannotSign.EXIT_CODE) public void signWith_certCannotSignCausesKeyCannotSign() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData {
public void signWith_certCannotSignCausesExit79() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData {
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign()); when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign());
File keyFile = File.createTempFile("sign-with", ".asc"); File keyFile = File.createTempFile("sign-with", ".asc");
File passwordFile = TestFileUtil.writeTempStringFile("dragon"); File passwordFile = TestFileUtil.writeTempStringFile("dragon");
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); assertKeyCannotSign(() ->
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(),
"--sign-with", keyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void signWith_badDataCausesBadData() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException {
when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
File keyFile = File.createTempFile("sign-with", ".asc"); File keyFile = File.createTempFile("sign-with", ".asc");
File passwordFile = TestFileUtil.writeTempStringFile("orange"); File passwordFile = TestFileUtil.writeTempStringFile("orange");
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); assertBadData(() ->
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(),
"--sign-with", keyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void cert_nonExistentCertFileCausesMissingInput() {
public void cert_nonExistentCertFileCausesExit61() { assertMissingInput(() ->
SopCLI.main(new String[] {"encrypt", "invalid.asc"}); SopCLI.execute("encrypt", "invalid.asc"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) public void cert_unsupportedAsymmetricAlgorithmCausesUnsupportedAsymAlg() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
public void cert_unsupportedAsymmetricAlgorithmCausesExit13() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception()));
File certFile = File.createTempFile("cert", ".asc"); File certFile = File.createTempFile("cert", ".asc");
SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); assertUnsupportedAsymmetricAlgo(() ->
SopCLI.execute("encrypt", certFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.CertCannotEncrypt.EXIT_CODE) public void cert_certCannotEncryptCausesCertCannotEncrypt() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
public void cert_certCannotEncryptCausesExit17() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.CertCannotEncrypt("Certificate cannot encrypt.", new Exception())); when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.CertCannotEncrypt("Certificate cannot encrypt.", new Exception()));
File certFile = File.createTempFile("cert", ".asc"); File certFile = File.createTempFile("cert", ".asc");
SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); assertCertCannotEncrypt(() ->
SopCLI.execute("encrypt", certFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void cert_badDataCausesBadData() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
public void cert_badDataCausesExit41() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData {
when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
File certFile = File.createTempFile("cert", ".asc"); File certFile = File.createTempFile("cert", ".asc");
SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); assertBadData(() ->
SopCLI.execute("encrypt", certFile.getAbsolutePath()));
} }
@Test @Test
public void noArmor_notCalledByDefault() throws IOException { public void noArmor_notCalledByDefault() throws IOException {
File passwordFile = TestFileUtil.writeTempStringFile("clownfish"); File passwordFile = TestFileUtil.writeTempStringFile("clownfish");
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath()));
verify(encrypt, never()).noArmor(); verify(encrypt, never()).noArmor();
} }
@Test @Test
public void noArmor_callGetsPassedDown() throws IOException { public void noArmor_callGetsPassedDown() throws IOException {
File passwordFile = TestFileUtil.writeTempStringFile("monkey"); File passwordFile = TestFileUtil.writeTempStringFile("monkey");
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--no-armor"}); assertSuccess(() ->
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath(), "--no-armor"));
verify(encrypt, times(1)).noArmor(); verify(encrypt, times(1)).noArmor();
} }
@Test @Test
@ExpectSystemExitWithStatus(1) public void writeTo_ioExceptionCausesGenericError() throws IOException {
public void writeTo_ioExceptionCausesExit1() throws IOException {
when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult<EncryptionResult>() { when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult<EncryptionResult>() {
@Override @Override
public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException { public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException {
@ -199,6 +219,7 @@ public class EncryptCmdTest {
} }
}); });
File passwordFile = TestFileUtil.writeTempStringFile("wildcat"); File passwordFile = TestFileUtil.writeTempStringFile("wildcat");
SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); assertGenericError(() ->
SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath()));
} }
} }

View file

@ -10,12 +10,14 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError;
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sop.Ready; import sop.Ready;
@ -45,32 +47,34 @@ public class ExtractCertCmdTest {
@Test @Test
public void noArmor_notCalledByDefault() { public void noArmor_notCalledByDefault() {
SopCLI.main(new String[] {"extract-cert"}); assertSuccess(() ->
SopCLI.execute("extract-cert"));
verify(extractCert, never()).noArmor(); verify(extractCert, never()).noArmor();
} }
@Test @Test
public void noArmor_passedDown() { public void noArmor_passedDown() {
SopCLI.main(new String[] {"extract-cert", "--no-armor"}); assertSuccess(() ->
SopCLI.execute("extract-cert", "--no-armor"));
verify(extractCert, times(1)).noArmor(); verify(extractCert, times(1)).noArmor();
} }
@Test @Test
@ExpectSystemExitWithStatus(1) public void key_ioExceptionCausesGenericError() throws IOException, SOPGPException.BadData {
public void key_ioExceptionCausesExit1() throws IOException, SOPGPException.BadData {
when(extractCert.key((InputStream) any())).thenReturn(new Ready() { when(extractCert.key((InputStream) any())).thenReturn(new Ready() {
@Override @Override
public void writeTo(OutputStream outputStream) throws IOException { public void writeTo(OutputStream outputStream) throws IOException {
throw new IOException(); throw new IOException();
} }
}); });
SopCLI.main(new String[] {"extract-cert"}); assertGenericError(() ->
SopCLI.execute("extract-cert"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void key_badDataCausesBadData() throws IOException, SOPGPException.BadData {
public void key_badDataCausesExit41() throws IOException, SOPGPException.BadData {
when(extractCert.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(extractCert.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"extract-cert"}); assertBadData(() ->
SopCLI.execute("extract-cert"));
} }
} }

View file

@ -10,11 +10,14 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError;
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg;
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedAsymmetricAlgo;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InOrder; import org.mockito.InOrder;
@ -47,19 +50,22 @@ public class GenerateKeyCmdTest {
@Test @Test
public void noArmor_notCalledByDefault() { public void noArmor_notCalledByDefault() {
SopCLI.main(new String[] {"generate-key", "Alice"}); assertSuccess(() ->
SopCLI.execute("generate-key", "Alice"));
verify(generateKey, never()).noArmor(); verify(generateKey, never()).noArmor();
} }
@Test @Test
public void noArmor_passedDown() { public void noArmor_passedDown() {
SopCLI.main(new String[] {"generate-key", "--no-armor", "Alice"}); assertSuccess(() ->
SopCLI.execute("generate-key", "--no-armor", "Alice"));
verify(generateKey, times(1)).noArmor(); verify(generateKey, times(1)).noArmor();
} }
@Test @Test
public void userId_multipleUserIdsPassedDownInProperOrder() { public void userId_multipleUserIdsPassedDownInProperOrder() {
SopCLI.main(new String[] {"generate-key", "Alice <alice@pgpainless.org>", "Bob <bob@pgpainless.org>"}); assertSuccess(() ->
SopCLI.execute("generate-key", "Alice <alice@pgpainless.org>", "Bob <bob@pgpainless.org>"));
InOrder inOrder = Mockito.inOrder(generateKey); InOrder inOrder = Mockito.inOrder(generateKey);
inOrder.verify(generateKey).userId("Alice <alice@pgpainless.org>"); inOrder.verify(generateKey).userId("Alice <alice@pgpainless.org>");
@ -69,30 +75,32 @@ public class GenerateKeyCmdTest {
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE)
public void missingArgumentCausesExit19() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { public void missingArgumentCausesExit19() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
// TODO: RFC4880-bis and the current Stateless OpenPGP CLI spec allow keys to have no user-ids, // TODO: RFC4880-bis and the current Stateless OpenPGP CLI spec allow keys to have no user-ids,
// so we might want to change this test in the future. // so we might want to change this test in the future.
when(generateKey.generate()).thenThrow(new SOPGPException.MissingArg("Missing user-id.")); when(generateKey.generate()).thenThrow(new SOPGPException.MissingArg("Missing user-id."));
SopCLI.main(new String[] {"generate-key"}); assertMissingArg(() ->
SopCLI.execute("generate-key"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE)
public void unsupportedAsymmetricAlgorithmCausesExit13() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { public void unsupportedAsymmetricAlgorithmCausesExit13() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
when(generateKey.generate()).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); when(generateKey.generate()).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception()));
SopCLI.main(new String[] {"generate-key", "Alice"}); assertUnsupportedAsymmetricAlgo(() ->
SopCLI.execute("generate-key", "Alice"));
} }
@Test @Test
@ExpectSystemExitWithStatus(1) public void ioExceptionCausesGenericError() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
public void ioExceptionCausesExit1() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException {
when(generateKey.generate()).thenReturn(new Ready() { when(generateKey.generate()).thenReturn(new Ready() {
@Override @Override
public void writeTo(OutputStream outputStream) throws IOException { public void writeTo(OutputStream outputStream) throws IOException {
throw new IOException(); throw new IOException();
} }
}); });
SopCLI.main(new String[] {"generate-key", "Alice"});
assertGenericError(() ->
SopCLI.execute("generate-key", "Alice"));
} }
} }

View file

@ -4,7 +4,6 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sop.ReadyWithResult; import sop.ReadyWithResult;
@ -26,6 +25,8 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg;
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
public class InlineDetachCmdTest { public class InlineDetachCmdTest {
@ -41,9 +42,9 @@ public class InlineDetachCmdTest {
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void testMissingSignaturesOutResultsInMissingArg() {
public void testMissingSignaturesOutResultsInExit19() { assertMissingArg(() ->
SopCLI.main(new String[] {"inline-detach"}); SopCLI.execute("inline-detach"));
} }
@Test @Test
@ -67,7 +68,8 @@ public class InlineDetachCmdTest {
} }
}); });
SopCLI.main(new String[] {"inline-detach", "--signatures-out", tempFile.getAbsolutePath(), "--no-armor"}); assertSuccess(() ->
SopCLI.execute("inline-detach", "--signatures-out", tempFile.getAbsolutePath(), "--no-armor"));
verify(inlineDetach, times(1)).noArmor(); verify(inlineDetach, times(1)).noArmor();
verify(inlineDetach, times(1)).message((InputStream) any()); verify(inlineDetach, times(1)).message((InputStream) any());
} }

View file

@ -10,13 +10,20 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
import static sop.testsuite.assertions.SopExecutionAssertions.assertExpectedText;
import static sop.testsuite.assertions.SopExecutionAssertions.assertGenericError;
import static sop.testsuite.assertions.SopExecutionAssertions.assertKeyIsProtected;
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingArg;
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput;
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sop.ReadyWithResult; import sop.ReadyWithResult;
@ -54,70 +61,77 @@ public class SignCmdTest {
@Test @Test
public void as_optionsAreCaseInsensitive() { public void as_optionsAreCaseInsensitive() {
SopCLI.main(new String[] {"sign", "--as", "Binary", keyFile.getAbsolutePath()}); assertSuccess(() ->
SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); SopCLI.execute("sign", "--as", "Binary", keyFile.getAbsolutePath()));
SopCLI.main(new String[] {"sign", "--as", "BINARY", keyFile.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("sign", "--as", "binary", keyFile.getAbsolutePath()));
assertSuccess(() ->
SopCLI.execute("sign", "--as", "BINARY", keyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void as_invalidOptionCausesExit37() { public void as_invalidOptionCausesExit37() {
SopCLI.main(new String[] {"sign", "--as", "Invalid", keyFile.getAbsolutePath()}); assertUnsupportedOption(() ->
SopCLI.execute("sign", "--as", "Invalid", keyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
when(detachedSign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting signing mode not supported.")); when(detachedSign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting signing mode not supported."));
SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); assertUnsupportedOption(() ->
SopCLI.execute("sign", "--as", "binary", keyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
public void key_nonExistentKeyFileCausesExit61() { public void key_nonExistentKeyFileCausesExit61() {
SopCLI.main(new String[] {"sign", "invalid.asc"}); assertMissingInput(() ->
SopCLI.execute("sign", "invalid.asc"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE)
public void key_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { public void key_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData {
when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected());
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); assertKeyIsProtected(() ->
SopCLI.execute("sign", keyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData {
when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); assertBadData(() ->
SopCLI.execute("sign", keyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE)
public void key_missingKeyFileCausesExit19() { public void key_missingKeyFileCausesExit19() {
SopCLI.main(new String[] {"sign"}); assertMissingArg(() ->
SopCLI.execute("sign"));
} }
@Test @Test
public void noArmor_notCalledByDefault() { public void noArmor_notCalledByDefault() {
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("sign", keyFile.getAbsolutePath()));
verify(detachedSign, never()).noArmor(); verify(detachedSign, never()).noArmor();
} }
@Test @Test
public void noArmor_passedDown() { public void noArmor_passedDown() {
SopCLI.main(new String[] {"sign", "--no-armor", keyFile.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("sign", "--no-armor", keyFile.getAbsolutePath()));
verify(detachedSign, times(1)).noArmor(); verify(detachedSign, times(1)).noArmor();
} }
@Test @Test
public void withKeyPassword_passedDown() { public void withKeyPassword_passedDown() {
SopCLI.main(new String[] {"sign", "--with-key-password", passFile.getAbsolutePath(), keyFile.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("sign",
"--with-key-password", passFile.getAbsolutePath(),
keyFile.getAbsolutePath()));
verify(detachedSign, times(1)).withKeyPassword("sw0rdf1sh"); verify(detachedSign, times(1)).withKeyPassword("sw0rdf1sh");
} }
@Test @Test
@ExpectSystemExitWithStatus(1)
public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText { public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText {
when(detachedSign.data((InputStream) any())).thenReturn(new ReadyWithResult<SigningResult>() { when(detachedSign.data((InputStream) any())).thenReturn(new ReadyWithResult<SigningResult>() {
@Override @Override
@ -125,13 +139,14 @@ public class SignCmdTest {
throw new IOException(); throw new IOException();
} }
}); });
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); assertGenericError(() ->
SopCLI.execute("sign", keyFile.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.ExpectedText.EXIT_CODE)
public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText { public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText {
when(detachedSign.data((InputStream) any())).thenThrow(new SOPGPException.ExpectedText()); when(detachedSign.data((InputStream) any())).thenThrow(new SOPGPException.ExpectedText());
SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); assertExpectedText(() ->
SopCLI.execute("sign", keyFile.getAbsolutePath()));
} }
} }

View file

@ -10,6 +10,11 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static sop.testsuite.assertions.SopExecutionAssertions.assertBadData;
import static sop.testsuite.assertions.SopExecutionAssertions.assertMissingInput;
import static sop.testsuite.assertions.SopExecutionAssertions.assertNoSignature;
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
@ -21,7 +26,6 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -76,60 +80,75 @@ public class VerifyCmdTest {
@Test @Test
public void notAfter_passedDown() throws SOPGPException.UnsupportedOption, ParseException { public void notAfter_passedDown() throws SOPGPException.UnsupportedOption, ParseException {
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"); Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z");
SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("verify", "--not-after", "2019-10-29T18:36:45Z",
signature.getAbsolutePath(), cert.getAbsolutePath()));
verify(detachedVerify, times(1)).notAfter(date); verify(detachedVerify, times(1)).notAfter(date);
} }
@Test @Test
public void notAfter_now() throws SOPGPException.UnsupportedOption { public void notAfter_now() throws SOPGPException.UnsupportedOption {
Date now = new Date(); Date now = new Date();
SopCLI.main(new String[] {"verify", "--not-after", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("verify", "--not-after", "now",
signature.getAbsolutePath(), cert.getAbsolutePath()));
verify(detachedVerify, times(1)).notAfter(dateMatcher(now)); verify(detachedVerify, times(1)).notAfter(dateMatcher(now));
} }
@Test @Test
public void notAfter_dashCountsAsEndOfTime() throws SOPGPException.UnsupportedOption { public void notAfter_dashCountsAsEndOfTime() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"verify", "--not-after", "-", signature.getAbsolutePath(), cert.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("verify", "--not-after", "-",
signature.getAbsolutePath(), cert.getAbsolutePath()));
verify(detachedVerify, times(1)).notAfter(AbstractSopCmd.END_OF_TIME); verify(detachedVerify, times(1)).notAfter(AbstractSopCmd.END_OF_TIME);
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
when(detachedVerify.notAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); when(detachedVerify.notAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported."));
SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); assertUnsupportedOption(() ->
SopCLI.execute("verify", "--not-after", "2019-10-29T18:36:45Z",
signature.getAbsolutePath(), cert.getAbsolutePath()));
} }
@Test @Test
public void notBefore_passedDown() throws SOPGPException.UnsupportedOption, ParseException { public void notBefore_passedDown() throws SOPGPException.UnsupportedOption, ParseException {
Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z"); Date date = UTCUtil.parseUTCDate("2019-10-29T18:36:45Z");
SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("verify", "--not-before", "2019-10-29T18:36:45Z",
signature.getAbsolutePath(), cert.getAbsolutePath()));
verify(detachedVerify, times(1)).notBefore(date); verify(detachedVerify, times(1)).notBefore(date);
} }
@Test @Test
public void notBefore_now() throws SOPGPException.UnsupportedOption { public void notBefore_now() throws SOPGPException.UnsupportedOption {
Date now = new Date(); Date now = new Date();
SopCLI.main(new String[] {"verify", "--not-before", "now", signature.getAbsolutePath(), cert.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("verify", "--not-before", "now",
signature.getAbsolutePath(), cert.getAbsolutePath()));
verify(detachedVerify, times(1)).notBefore(dateMatcher(now)); verify(detachedVerify, times(1)).notBefore(dateMatcher(now));
} }
@Test @Test
public void notBefore_dashCountsAsBeginningOfTime() throws SOPGPException.UnsupportedOption { public void notBefore_dashCountsAsBeginningOfTime() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"verify", "--not-before", "-", signature.getAbsolutePath(), cert.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("verify", "--not-before", "-",
signature.getAbsolutePath(), cert.getAbsolutePath()));
verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME); verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME);
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE)
public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption {
when(detachedVerify.notBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); when(detachedVerify.notBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported."));
SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); assertUnsupportedOption(() ->
SopCLI.execute("verify", "--not-before", "2019-10-29T18:36:45Z",
signature.getAbsolutePath(), cert.getAbsolutePath()));
} }
@Test @Test
public void notBeforeAndNotAfterAreCalledWithDefaultValues() throws SOPGPException.UnsupportedOption { public void notBeforeAndNotAfterAreCalledWithDefaultValues() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath()));
verify(detachedVerify, times(1)).notAfter(dateMatcher(new Date())); verify(detachedVerify, times(1)).notAfter(dateMatcher(new Date()));
verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME); verify(detachedVerify, times(1)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME);
} }
@ -139,43 +158,43 @@ public class VerifyCmdTest {
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
public void cert_fileNotFoundCausesExit61() { public void cert_fileNotFoundCausesExit61() {
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), "invalid.asc"}); assertMissingInput(() ->
SopCLI.execute("verify", signature.getAbsolutePath(), "invalid.asc"));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void cert_badDataCausesExit41() throws SOPGPException.BadData, IOException { public void cert_badDataCausesExit41() throws SOPGPException.BadData, IOException {
when(detachedVerify.cert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(detachedVerify.cert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); assertBadData(() ->
SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE)
public void signature_fileNotFoundCausesExit61() { public void signature_fileNotFoundCausesExit61() {
SopCLI.main(new String[] {"verify", "invalid.sig", cert.getAbsolutePath()}); assertMissingInput(() ->
SopCLI.execute("verify", "invalid.sig", cert.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void signature_badDataCausesExit41() throws SOPGPException.BadData, IOException { public void signature_badDataCausesExit41() throws SOPGPException.BadData, IOException {
when(detachedVerify.signatures((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(detachedVerify.signatures((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); assertBadData(() ->
SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.NoSignature.EXIT_CODE)
public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.NoSignature()); when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.NoSignature());
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); assertNoSignature(() ->
SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath()));
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE)
public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData {
when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); when(detachedVerify.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException()));
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); assertBadData(() ->
SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath()));
} }
@Test @Test
@ -192,7 +211,8 @@ public class VerifyCmdTest {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out)); System.setOut(new PrintStream(out));
SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), cert.getAbsolutePath()}); assertSuccess(() ->
SopCLI.execute("verify", signature.getAbsolutePath(), cert.getAbsolutePath()));
System.setOut(originalSout); System.setOut(originalSout);

View file

@ -4,19 +4,19 @@
package sop.cli.picocli.commands; package sop.cli.picocli.commands;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import sop.SOP; import sop.SOP;
import sop.cli.picocli.SopCLI; import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Version; import sop.operation.Version;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess;
import static sop.testsuite.assertions.SopExecutionAssertions.assertUnsupportedOption;
public class VersionCmdTest { public class VersionCmdTest {
private Version version; private Version version;
@ -29,6 +29,8 @@ public class VersionCmdTest {
when(version.getVersion()).thenReturn("1.0"); when(version.getVersion()).thenReturn("1.0");
when(version.getExtendedVersion()).thenReturn("MockSop Extended Version Information"); when(version.getExtendedVersion()).thenReturn("MockSop Extended Version Information");
when(version.getBackendVersion()).thenReturn("Foo"); when(version.getBackendVersion()).thenReturn("Foo");
when(version.getSopSpecVersion()).thenReturn("draft-dkg-openpgp-stateless-cli-XX");
when(version.getSopVVersion()).thenReturn("1.0");
when(sop.version()).thenReturn(version); when(sop.version()).thenReturn(version);
SopCLI.setSopInstance(sop); SopCLI.setSopInstance(sop);
@ -36,26 +38,41 @@ public class VersionCmdTest {
@Test @Test
public void assertVersionCommandWorks() { public void assertVersionCommandWorks() {
SopCLI.main(new String[] {"version"}); assertSuccess(() ->
SopCLI.execute("version"));
verify(version, times(1)).getVersion(); verify(version, times(1)).getVersion();
verify(version, times(1)).getName(); verify(version, times(1)).getName();
} }
@Test @Test
public void assertExtendedVersionCommandWorks() { public void assertExtendedVersionCommandWorks() {
SopCLI.main(new String[] {"version", "--extended"}); assertSuccess(() ->
SopCLI.execute("version", "--extended"));
verify(version, times(1)).getExtendedVersion(); verify(version, times(1)).getExtendedVersion();
} }
@Test @Test
public void assertBackendVersionCommandWorks() { public void assertBackendVersionCommandWorks() {
SopCLI.main(new String[] {"version", "--backend"}); assertSuccess(() ->
SopCLI.execute("version", "--backend"));
verify(version, times(1)).getBackendVersion(); verify(version, times(1)).getBackendVersion();
} }
@Test @Test
@ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertSpecVersionCommandWorks() {
assertSuccess(() ->
SopCLI.execute("version", "--sop-spec"));
}
@Test
public void assertSOPVVersionCommandWorks() {
assertSuccess(() ->
SopCLI.execute("version", "--sopv"));
}
@Test
public void assertInvalidOptionResultsInExit37() { public void assertInvalidOptionResultsInExit37() {
SopCLI.main(new String[] {"version", "--invalid"}); assertUnsupportedOption(() ->
SopCLI.execute("version", "--invalid"));
} }
} }

View file

@ -4,21 +4,7 @@
package sop package sop
import sop.operation.Armor import sop.operation.*
import sop.operation.ChangeKeyPassword
import sop.operation.Dearmor
import sop.operation.Decrypt
import sop.operation.DetachedSign
import sop.operation.DetachedVerify
import sop.operation.Encrypt
import sop.operation.ExtractCert
import sop.operation.GenerateKey
import sop.operation.InlineDetach
import sop.operation.InlineSign
import sop.operation.InlineVerify
import sop.operation.ListProfiles
import sop.operation.RevokeKey
import sop.operation.Version
/** /**
* Stateless OpenPGP Interface. This class provides a stateless interface to various OpenPGP related * Stateless OpenPGP Interface. This class provides a stateless interface to various OpenPGP related
@ -26,72 +12,63 @@ import sop.operation.Version
* intended for reuse. If you for example need to generate multiple keys, make a dedicated call to * intended for reuse. If you for example need to generate multiple keys, make a dedicated call to
* [generateKey] once per key generation. * [generateKey] once per key generation.
*/ */
interface SOP { interface SOP : SOPV {
/** Get information about the implementations name and version. */
fun version(): Version
/** Generate a secret key. */ /** Generate a secret key. */
fun generateKey(): GenerateKey fun generateKey(): GenerateKey?
/** Extract a certificate (public key) from a secret key. */ /** Extract a certificate (public key) from a secret key. */
fun extractCert(): ExtractCert fun extractCert(): ExtractCert?
/** /**
* Create detached signatures. If you want to sign a message inline, use [inlineSign] instead. * Create detached signatures. If you want to sign a message inline, use [inlineSign] instead.
*/ */
fun sign(): DetachedSign = detachedSign() fun sign(): DetachedSign? = detachedSign()
/** /**
* Create detached signatures. If you want to sign a message inline, use [inlineSign] instead. * Create detached signatures. If you want to sign a message inline, use [inlineSign] instead.
*/ */
fun detachedSign(): DetachedSign fun detachedSign(): DetachedSign?
/** /**
* Sign a message using inline signatures. If you need to create detached signatures, use * Sign a message using inline signatures. If you need to create detached signatures, use
* [detachedSign] instead. * [detachedSign] instead.
*/ */
fun inlineSign(): InlineSign fun inlineSign(): InlineSign?
/**
* Verify detached signatures. If you need to verify an inline-signed message, use
* [inlineVerify] instead.
*/
fun verify(): DetachedVerify = detachedVerify()
/**
* Verify detached signatures. If you need to verify an inline-signed message, use
* [inlineVerify] instead.
*/
fun detachedVerify(): DetachedVerify
/**
* Verify signatures of an inline-signed message. If you need to verify detached signatures over
* a message, use [detachedVerify] instead.
*/
fun inlineVerify(): InlineVerify
/** Detach signatures from an inline signed message. */ /** Detach signatures from an inline signed message. */
fun inlineDetach(): InlineDetach fun inlineDetach(): InlineDetach?
/** Encrypt a message. */ /** Encrypt a message. */
fun encrypt(): Encrypt fun encrypt(): Encrypt?
/** Decrypt a message. */ /** Decrypt a message. */
fun decrypt(): Decrypt fun decrypt(): Decrypt?
/** Convert binary OpenPGP data to ASCII. */ /** Convert binary OpenPGP data to ASCII. */
fun armor(): Armor fun armor(): Armor?
/** Converts ASCII armored OpenPGP data to binary. */ /** Converts ASCII armored OpenPGP data to binary. */
fun dearmor(): Dearmor fun dearmor(): Dearmor?
/** List supported [Profiles][Profile] of a subcommand. */ /** List supported [Profiles][Profile] of a subcommand. */
fun listProfiles(): ListProfiles fun listProfiles(): ListProfiles?
/** Revoke one or more secret keys. */ /** Revoke one or more secret keys. */
fun revokeKey(): RevokeKey fun revokeKey(): RevokeKey?
/** Update a key's password. */ /** Update a key's password. */
fun changeKeyPassword(): ChangeKeyPassword fun changeKeyPassword(): ChangeKeyPassword?
/** Keep a secret key up-to-date. */
fun updateKey(): UpdateKey?
/** Merge OpenPGP certificates. */
fun mergeCerts(): MergeCerts?
/** Certify OpenPGP Certificate User-IDs. */
fun certifyUserId(): CertifyUserId?
/** Validate a UserID in an OpenPGP certificate. */
fun validateUserId(): ValidateUserId?
} }

View file

@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import sop.operation.DetachedVerify
import sop.operation.InlineVerify
import sop.operation.Version
/** Subset of [SOP] implementing only OpenPGP signature verification. */
interface SOPV {
/** Get information about the implementations name and version. */
fun version(): Version?
/**
* Verify detached signatures. If you need to verify an inline-signed message, use
* [inlineVerify] instead.
*/
fun verify(): DetachedVerify? = detachedVerify()
/**
* Verify detached signatures. If you need to verify an inline-signed message, use
* [inlineVerify] instead.
*/
fun detachedVerify(): DetachedVerify?
/**
* Verify signatures of an inline-signed message. If you need to verify detached signatures over
* a message, use [detachedVerify] instead.
*/
fun inlineVerify(): InlineVerify?
}

View file

@ -1,14 +0,0 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.enums
@Deprecated("Use of armor labels is deprecated.")
enum class ArmorLabel {
auto,
sig,
key,
cert,
message
}

View file

@ -305,4 +305,77 @@ abstract class SOPGPException : RuntimeException {
const val EXIT_CODE = 89 const val EXIT_CODE = 89
} }
} }
/**
* The sop implementation supports some form of hardware-backed secret keys, but could not
* identify the hardware device.
*/
class NoHardwareKeyFound : SOPGPException {
constructor() : super()
constructor(errorMsg: String) : super(errorMsg)
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 97
}
}
/**
* The sop implementation tried to use a hardware-backed secret key, but the cryptographic
* hardware refused the operation for some reason other than a bad PIN or password.
*/
class HardwareKeyFailure : SOPGPException {
constructor() : super()
constructor(errorMsg: String) : super(errorMsg)
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 101
}
}
/** The primary key of a KEYS object is too weak or revoked. */
class PrimaryKeyBad : SOPGPException {
constructor() : super()
constructor(errorMsg: String) : super(errorMsg)
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 103
}
}
/** The CERTS object has no matching User ID. */
class CertUserIdNoMatch : SOPGPException {
val fingerprint: ByteArray?
constructor() : super() {
fingerprint = null
}
constructor(fingerprint: ByteArray) : super() {
this.fingerprint = fingerprint
}
constructor(errorMsg: String) : super(errorMsg) {
fingerprint = null
}
constructor(errorMsg: String, cause: Throwable) : super(errorMsg, cause) {
fingerprint = null
}
override fun getExitCode(): Int = EXIT_CODE
companion object {
const val EXIT_CODE = 107
}
}
} }

View file

@ -7,22 +7,10 @@ package sop.operation
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import sop.Ready import sop.Ready
import sop.enums.ArmorLabel
import sop.exception.SOPGPException.BadData import sop.exception.SOPGPException.BadData
import sop.exception.SOPGPException.UnsupportedOption
interface Armor { interface Armor {
/**
* Overrides automatic detection of label.
*
* @param label armor label
* @return builder instance
*/
@Deprecated("Use of armor labels is deprecated and will be removed in a future release.")
@Throws(UnsupportedOption::class)
fun label(label: ArmorLabel): Armor
/** /**
* Armor the provided data. * Armor the provided data.
* *

View file

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import sop.Ready
import sop.exception.SOPGPException
import sop.util.UTF8Util
interface CertifyUserId {
@Throws(SOPGPException.UnsupportedOption::class) fun noArmor(): CertifyUserId
@Throws(SOPGPException.UnsupportedOption::class) fun userId(userId: String): CertifyUserId
@Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class)
fun withKeyPassword(password: String): CertifyUserId =
withKeyPassword(password.toByteArray(UTF8Util.UTF8))
@Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class)
fun withKeyPassword(password: ByteArray): CertifyUserId
@Throws(SOPGPException.UnsupportedOption::class) fun noRequireSelfSig(): CertifyUserId
@Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class)
fun keys(keys: InputStream): CertifyUserId
@Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class)
fun keys(keys: ByteArray): CertifyUserId = keys(keys.inputStream())
@Throws(
SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class)
fun certs(certs: InputStream): Ready
@Throws(
SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class)
fun certs(certs: ByteArray): Ready = certs(certs.inputStream())
}

View file

@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import sop.Ready
import sop.exception.SOPGPException
interface MergeCerts {
@Throws(SOPGPException.UnsupportedOption::class) fun noArmor(): MergeCerts
@Throws(SOPGPException.BadData::class, IOException::class)
fun updates(updateCerts: InputStream): MergeCerts
@Throws(SOPGPException.BadData::class, IOException::class)
fun updates(updateCerts: ByteArray): MergeCerts = updates(updateCerts.inputStream())
@Throws(SOPGPException.BadData::class, IOException::class)
fun baseCertificates(certs: InputStream): Ready
@Throws(SOPGPException.BadData::class, IOException::class)
fun baseCertificates(certs: ByteArray): Ready = baseCertificates(certs.inputStream())
}

View file

@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import sop.Ready
import sop.exception.SOPGPException
import sop.util.UTF8Util
interface UpdateKey {
/**
* Disable ASCII armor encoding of the output.
*
* @return builder instance
*/
fun noArmor(): UpdateKey
@Throws(SOPGPException.UnsupportedOption::class) fun signingOnly(): UpdateKey
@Throws(SOPGPException.UnsupportedOption::class) fun noNewMechanisms(): UpdateKey
@Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class)
fun withKeyPassword(password: String): UpdateKey =
withKeyPassword(password.toByteArray(UTF8Util.UTF8))
@Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class)
fun withKeyPassword(password: ByteArray): UpdateKey
@Throws(
SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class)
fun mergeCerts(certs: InputStream): UpdateKey
@Throws(
SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class)
fun mergeCerts(certs: ByteArray): UpdateKey = mergeCerts(certs.inputStream())
@Throws(
SOPGPException.BadData::class,
IOException::class,
SOPGPException.KeyIsProtected::class,
SOPGPException.PrimaryKeyBad::class)
fun key(key: InputStream): Ready
@Throws(
SOPGPException.BadData::class,
IOException::class,
SOPGPException.KeyIsProtected::class,
SOPGPException.PrimaryKeyBad::class)
fun key(key: ByteArray): Ready = key(key.inputStream())
}

View file

@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.operation
import java.io.IOException
import java.io.InputStream
import sop.exception.SOPGPException
/** Subcommand to validate UserIDs on certificates. */
interface ValidateUserId {
/**
* If this is set, then the USERID is treated as an e-mail address, and matched only against the
* e-mail address part of each correctly bound User ID. The rest of each correctly bound User ID
* is ignored.
*
* @return this
*/
@Throws(SOPGPException.UnsupportedOption::class) fun addrSpecOnly(): ValidateUserId
/**
* Set the UserID to validate. To match only the email address, call [addrSpecOnly].
*
* @param userId UserID or email address
* @return this
*/
fun userId(userId: String): ValidateUserId
/**
* Add certificates, which act as authorities. The [userId] is only considered correctly bound,
* if it was bound by an authoritative certificate.
*
* @param certs authoritative certificates
* @return this
*/
@Throws(SOPGPException.BadData::class, IOException::class)
fun authorities(certs: InputStream): ValidateUserId
/**
* Add certificates, which act as authorities. The [userId] is only considered correctly bound,
* if it was bound by an authoritative certificate.
*
* @param certs authoritative certificates
* @return this
*/
@Throws(SOPGPException.BadData::class, IOException::class)
fun authorities(certs: ByteArray): ValidateUserId = authorities(certs.inputStream())
/**
* Add subject certificates, on which UserID bindings are validated.
*
* @param certs subject certificates
* @return true if all subject certificates have a correct binding to the UserID.
* @throws SOPGPException.BadData if the subject certificates are malformed
* @throws IOException if a parser exception happens
* @throws SOPGPException.CertUserIdNoMatch if any subject certificate does not have a correctly
* bound UserID that matches [userId].
*/
@Throws(
SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class)
fun subjects(certs: InputStream): Boolean
/**
* Add subject certificates, on which UserID bindings are validated.
*
* @param certs subject certificates
* @return true if all subject certificates have a correct binding to the UserID.
* @throws SOPGPException.BadData if the subject certificates are malformed
* @throws IOException if a parser exception happens
* @throws SOPGPException.CertUserIdNoMatch if any subject certificate does not have a correctly
* bound UserID that matches [userId].
*/
@Throws(
SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class)
fun subjects(certs: ByteArray): Boolean = subjects(certs.inputStream())
}

View file

@ -4,6 +4,9 @@
package sop.operation package sop.operation
import kotlin.jvm.Throws
import sop.exception.SOPGPException
interface Version { interface Version {
/** /**
@ -97,4 +100,11 @@ interface Version {
* @return remarks or null * @return remarks or null
*/ */
fun getSopSpecImplementationRemarks(): String? fun getSopSpecImplementationRemarks(): String?
/**
* Return the single-line SEMVER version of the sopv interface subset it provides complete
* coverage of. If the implementation does not provide complete coverage for any sopv interface,
* this method throws an [SOPGPException.UnsupportedOption] instead.
*/
@Throws(SOPGPException.UnsupportedOption::class) fun getSopVVersion(): String
} }

View file

@ -15,6 +15,7 @@ import java.io.OutputStream
* class is useful if we need to provide an [OutputStream] at one point in time when the final * class is useful if we need to provide an [OutputStream] at one point in time when the final
* target output stream is not yet known. * target output stream is not yet known.
*/ */
@Deprecated("Marked for removal.")
class ProxyOutputStream : OutputStream() { class ProxyOutputStream : OutputStream() {
private val buffer = ByteArrayOutputStream() private val buffer = ByteArrayOutputStream()
private var swapped: OutputStream? = null private var swapped: OutputStream? = null

View file

@ -0,0 +1,235 @@
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop.testsuite.assertions;
import sop.exception.SOPGPException;
import java.util.function.IntSupplier;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
/**
* DSL for testing the return values of SOP method calls.
*/
public class SopExecutionAssertions {
/**
* Assert that the execution of the given function returns 0.
*
* @param function function to execute
*/
public static void assertSuccess(IntSupplier function) {
assertEquals(0, function.getAsInt());
}
/**
* Assert that the execution of the given function returns a generic error with error code 1.
*
* @param function function to execute.
*/
public static void assertGenericError(IntSupplier function) {
assertEquals(1, function.getAsInt());
}
/**
* Assert that the execution of the given function returns a non-zero error code.
*
* @param function function to execute
*/
public static void assertAnyError(IntSupplier function) {
assertNotEquals(0, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 3
* (which corresponds to {@link sop.exception.SOPGPException.NoSignature}).
*
* @param function function to execute.
*/
public static void assertNoSignature(IntSupplier function) {
assertEquals(SOPGPException.NoSignature.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 13
* (which corresponds to {@link sop.exception.SOPGPException.UnsupportedAsymmetricAlgo}).
*
* @param function function to execute.
*/
public static void assertUnsupportedAsymmetricAlgo(IntSupplier function) {
assertEquals(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 17
* (which corresponds to {@link sop.exception.SOPGPException.CertCannotEncrypt}).
*
* @param function function to execute.
*/
public static void assertCertCannotEncrypt(IntSupplier function) {
assertEquals(SOPGPException.CertCannotEncrypt.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 19
* (which corresponds to {@link sop.exception.SOPGPException.MissingArg}).
*
* @param function function to execute.
*/
public static void assertMissingArg(IntSupplier function) {
assertEquals(SOPGPException.MissingArg.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 23
* (which corresponds to {@link sop.exception.SOPGPException.IncompleteVerification}).
*
* @param function function to execute.
*/
public static void assertIncompleteVerification(IntSupplier function) {
assertEquals(SOPGPException.IncompleteVerification.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 29
* (which corresponds to {@link sop.exception.SOPGPException.CannotDecrypt}).
*
* @param function function to execute.
*/
public static void assertCannotDecrypt(IntSupplier function) {
assertEquals(SOPGPException.CannotDecrypt.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 31
* (which corresponds to {@link sop.exception.SOPGPException.PasswordNotHumanReadable}).
*
* @param function function to execute.
*/
public static void assertPasswordNotHumanReadable(IntSupplier function) {
assertEquals(SOPGPException.PasswordNotHumanReadable.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 37
* (which corresponds to {@link sop.exception.SOPGPException.UnsupportedOption}).
*
* @param function function to execute.
*/
public static void assertUnsupportedOption(IntSupplier function) {
assertEquals(SOPGPException.UnsupportedOption.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 41
* (which corresponds to {@link sop.exception.SOPGPException.BadData}).
*
* @param function function to execute.
*/
public static void assertBadData(IntSupplier function) {
assertEquals(SOPGPException.BadData.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 53
* (which corresponds to {@link sop.exception.SOPGPException.ExpectedText}).
*
* @param function function to execute.
*/
public static void assertExpectedText(IntSupplier function) {
assertEquals(SOPGPException.ExpectedText.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 59
* (which corresponds to {@link sop.exception.SOPGPException.OutputExists}).
*
* @param function function to execute.
*/
public static void assertOutputExists(IntSupplier function) {
assertEquals(SOPGPException.OutputExists.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 61
* (which corresponds to {@link sop.exception.SOPGPException.MissingInput}).
*
* @param function function to execute.
*/
public static void assertMissingInput(IntSupplier function) {
assertEquals(SOPGPException.MissingInput.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 67
* (which corresponds to {@link sop.exception.SOPGPException.KeyIsProtected}).
*
* @param function function to execute.
*/
public static void assertKeyIsProtected(IntSupplier function) {
assertEquals(SOPGPException.KeyIsProtected.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 69
* (which corresponds to {@link sop.exception.SOPGPException.UnsupportedSubcommand}).
*
* @param function function to execute.
*/
public static void assertUnsupportedSubcommand(IntSupplier function) {
assertEquals(SOPGPException.UnsupportedSubcommand.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 71
* (which corresponds to {@link sop.exception.SOPGPException.UnsupportedSpecialPrefix}).
*
* @param function function to execute.
*/
public static void assertUnsupportedSpecialPrefix(IntSupplier function) {
assertEquals(SOPGPException.UnsupportedSpecialPrefix.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 73
* (which corresponds to {@link sop.exception.SOPGPException.AmbiguousInput}).
*
* @param function function to execute.
*/
public static void assertAmbiguousInput(IntSupplier function) {
assertEquals(SOPGPException.AmbiguousInput.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 79
* (which corresponds to {@link sop.exception.SOPGPException.KeyCannotSign}).
*
* @param function function to execute.
*/
public static void assertKeyCannotSign(IntSupplier function) {
assertEquals(SOPGPException.KeyCannotSign.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 83
* (which corresponds to {@link sop.exception.SOPGPException.IncompatibleOptions}).
*
* @param function function to execute.
*/
public static void assertIncompatibleOptions(IntSupplier function) {
assertEquals(SOPGPException.IncompatibleOptions.EXIT_CODE, function.getAsInt());
}
/**
* Assert that the execution of the given function returns error code 89
* (which corresponds to {@link sop.exception.SOPGPException.UnsupportedProfile}).
*
* @param function function to execute.
*/
public static void assertUnsupportedProfile(IntSupplier function) {
assertEquals(SOPGPException.UnsupportedProfile.EXIT_CODE, function.getAsInt());
}
}

View file

@ -327,4 +327,15 @@ public class EncryptDecryptTest extends AbstractSOPTest {
.toByteArrayAndResult() .toByteArrayAndResult()
.getBytes()); .getBytes());
} }
@ParameterizedTest
@MethodSource("provideInstances")
public void passingSecretKeysForPublicKeysFails(SOP sop) {
assertThrows(SOPGPException.BadData.class, () ->
sop.encrypt()
.withCert(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8))
.toByteArrayAndResult()
.getBytes());
}
} }

View file

@ -8,14 +8,15 @@ import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.opentest4j.TestAbortedException;
import sop.SOP; import sop.SOP;
import sop.exception.SOPGPException;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends")
public class VersionTest extends AbstractSOPTest { public class VersionTest extends AbstractSOPTest {
@ -59,7 +60,7 @@ public class VersionTest extends AbstractSOPTest {
try { try {
sop.version().getSopSpecVersion(); sop.version().getSopSpecVersion();
} catch (RuntimeException e) { } catch (RuntimeException e) {
assumeTrue(false); // SOP backend does not support this operation yet throw new TestAbortedException("SOP backend does not support 'version --sop-spec' yet.");
} }
String sopSpec = sop.version().getSopSpecVersion(); String sopSpec = sop.version().getSopSpecVersion();
@ -72,4 +73,17 @@ public class VersionTest extends AbstractSOPTest {
int sopRevision = sop.version().getSopSpecRevisionNumber(); int sopRevision = sop.version().getSopSpecRevisionNumber();
assertTrue(sop.version().getSopSpecRevisionName().endsWith("" + sopRevision)); assertTrue(sop.version().getSopSpecRevisionName().endsWith("" + sopRevision));
} }
@ParameterizedTest
@MethodSource("provideInstances")
public void sopVVersionTest(SOP sop) {
try {
sop.version().getSopVVersion();
} catch (SOPGPException.UnsupportedOption e) {
throw new TestAbortedException(
"Implementation does (gracefully) not provide coverage for any sopv interface version.");
} catch (RuntimeException e) {
throw new TestAbortedException("Implementation does not provide coverage for any sopv interface version.");
}
}
} }

View file

@ -4,7 +4,7 @@
allprojects { allprojects {
ext { ext {
shortVersion = '8.0.2' shortVersion = '11.0.0'
isSnapshot = true isSnapshot = true
minAndroidSdk = 10 minAndroidSdk = 10
javaSourceCompatibility = 1.8 javaSourceCompatibility = 1.8
@ -12,7 +12,7 @@ allprojects {
jsrVersion = '3.0.2' jsrVersion = '3.0.2'
junitVersion = '5.8.2' junitVersion = '5.8.2'
junitSysExitVersion = '1.1.2' junitSysExitVersion = '1.1.2'
logbackVersion = '1.2.11' logbackVersion = '1.2.13'
mockitoVersion = '4.5.1' mockitoVersion = '4.5.1'
picocliVersion = '4.6.3' picocliVersion = '4.6.3'
slf4jVersion = '1.7.36' slf4jVersion = '1.7.36'